├── .gitignore ├── migrations └── datastore │ ├── sqlite │ ├── 000005_create_wookie_table.down.sql │ ├── 000002_create_default_obj_types.down.sql │ ├── 000005_create_wookie_table.up.sql │ ├── 000001_init.down.sql │ ├── 000004_add_policy_column_to_warrant_table.up.sql │ ├── 000004_add_policy_column_to_warrant_table.down.sql │ ├── 000002_create_default_obj_types.up.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.up.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.down.sql │ └── README.md │ ├── mysql │ ├── 000005_create_wookie_table.down.sql │ ├── 000002_create_default_obj_types.down.sql │ ├── 000005_create_wookie_table.up.sql │ ├── 000001_init.down.sql │ ├── 000004_add_policy_column_to_warrant_table.up.sql │ ├── 000004_add_policy_column_to_warrant_table.down.sql │ ├── 000002_create_default_obj_types.up.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.down.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.up.sql │ └── README.md │ └── postgres │ ├── 000006_create_wookie_table.down.sql │ ├── 000002_create_default_obj_types.down.sql │ ├── 000006_create_wookie_table.up.sql │ ├── 000001_init.down.sql │ ├── 000004_add_policy_column_to_warrant_table.up.sql │ ├── 000004_add_policy_column_to_warrant_table.down.sql │ ├── 000002_create_default_obj_types.up.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.up.sql │ ├── 000003_update_default_pricing_tier_feature_object_types.down.sql │ ├── 000005_update_timestamp_column_types.down.sql │ ├── 000005_update_timestamp_column_types.up.sql │ └── README.md ├── .github ├── pull_request_template.md ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── release.yaml │ ├── go.yaml │ ├── sqlite.yaml │ ├── mysql.yaml │ └── postgres.yaml ├── NOTICE ├── tests ├── ci-apirunner.conf └── v1 │ ├── roles-crud.json │ ├── features-crud.json │ ├── pricing-tiers-crud.json │ └── permissions-crud.json ├── SECURITY.md ├── cmd └── warrant │ └── Makefile ├── Dockerfile ├── pkg ├── database │ ├── database.go │ └── sqlite-stub.go ├── service │ ├── service.go │ ├── middleware.go │ └── route.go ├── wookie │ ├── wookie_test.go │ ├── wookie.go │ └── token.go ├── authz │ ├── objecttype │ │ ├── list.go │ │ ├── repository.go │ │ └── model.go │ ├── warrant │ │ ├── subject.go │ │ ├── list.go │ │ ├── repository.go │ │ ├── subject_test.go │ │ └── policy.go │ ├── check │ │ ├── spec.go │ │ └── handlers.go │ └── query │ │ ├── list.go │ │ ├── spec.go │ │ └── handlers.go ├── object │ ├── role │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── tenant │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── feature │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── permission │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── pricingtier │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── user │ │ ├── list.go │ │ ├── spec.go │ │ ├── service.go │ │ └── handlers.go │ ├── list.go │ ├── spec.go │ ├── repository.go │ └── model.go └── stats │ └── stats.go ├── .goreleaser.yaml ├── CONTRIBUTING.md ├── .golangci.yaml ├── go.mod ├── development.md └── deployment.md /.gitignore: -------------------------------------------------------------------------------- 1 | warrant.yaml 2 | apirunner.conf 3 | .DS_Store 4 | bin/ 5 | dist/ 6 | .idea/ 7 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000005_create_wookie_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS wookie; 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Issue number and link (if applicable) 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Warrant 2 | Copyright 2024 WorkOS, Inc. 3 | 4 | This product includes software developed at 5 | WorkOS, Inc. 6 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000005_create_wookie_table.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DROP TABLE IF EXISTS wookie; 4 | 5 | COMMIT; 6 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000006_create_wookie_table.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DROP TABLE IF EXISTS wookie; 4 | 5 | COMMIT; 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /tests/ci-apirunner.conf: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8000", 3 | "headers": { 4 | "Authorization": "ApiKey warrant_api_key" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000002_create_default_obj_types.down.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM objectType 2 | WHERE typeId IN ('role', 'permission', 'tenant', 'user', 'pricing-tier', 'feature'); 3 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000002_create_default_obj_types.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DELETE FROM objectType 4 | WHERE typeId IN ('role', 'permission', 'tenant', 'user', 'pricing-tier', 'feature'); 5 | 6 | COMMIT; 7 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000002_create_default_obj_types.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DELETE FROM object_type 4 | WHERE type_id IN ('role', 'permission', 'tenant', 'user', 'pricing-tier', 'feature'); 5 | 6 | COMMIT; 7 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000005_create_wookie_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS wookie ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | ver INTEGER, 4 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000006_create_wookie_table.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE IF NOT EXISTS wookie ( 4 | id bigserial PRIMARY KEY, 5 | ver bigserial, 6 | created_at timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6) 7 | ); 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We recommend always using the latest version of Warrant in order to remain up-to-date with all security updates. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you discover a vulnerability, please contact us at security@workos.com. 10 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000005_create_wookie_table.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE IF NOT EXISTS wookie ( 4 | id bigint NOT NULL AUTO_INCREMENT, 5 | ver bigint NOT NULL, 6 | createdAt timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6), 7 | PRIMARY KEY (id) 8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 9 | 10 | COMMIT; 11 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pricingTier; 2 | DROP TABLE IF EXISTS feature; 3 | DROP TABLE IF EXISTS user; 4 | DROP TABLE IF EXISTS tenant; 5 | DROP TABLE IF EXISTS role; 6 | DROP TABLE IF EXISTS permission; 7 | DROP TABLE IF EXISTS object; 8 | DROP TABLE IF EXISTS context; 9 | DROP TABLE IF EXISTS warrant; 10 | DROP TABLE IF EXISTS objectType; 11 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DROP TABLE IF EXISTS pricingTier; 4 | DROP TABLE IF EXISTS feature; 5 | DROP TABLE IF EXISTS user; 6 | DROP TABLE IF EXISTS tenant; 7 | DROP TABLE IF EXISTS role; 8 | DROP TABLE IF EXISTS permission; 9 | DROP TABLE IF EXISTS object; 10 | DROP TABLE IF EXISTS context; 11 | DROP TABLE IF EXISTS warrant; 12 | DROP TABLE IF EXISTS objectType; 13 | 14 | COMMIT; 15 | -------------------------------------------------------------------------------- /cmd/warrant/Makefile: -------------------------------------------------------------------------------- 1 | NAME = warrant 2 | BUILD_PATH = bin/$(NAME) 3 | GOENV = GOARCH=amd64 GOOS=linux CGO_ENABLED=0 4 | GOCMD = go 5 | GOBUILD = $(GOCMD) build -v -o $(BUILD_PATH) 6 | 7 | .PHONY: clean 8 | clean: 9 | rm -f $(BUILD_PATH) 10 | 11 | .PHONY: dev 12 | dev: clean 13 | $(GOCMD) get 14 | $(GOBUILD) -tags="sqlite" main.go 15 | 16 | .PHONY: build 17 | build: clean 18 | $(GOCMD) get 19 | $(GOENV) $(GOBUILD) -ldflags="-s -w" main.go 20 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | DROP TABLE IF EXISTS pricing_tier; 4 | DROP TABLE IF EXISTS feature; 5 | DROP TABLE IF EXISTS "user"; 6 | DROP TABLE IF EXISTS tenant; 7 | DROP TABLE IF EXISTS role; 8 | DROP TABLE IF EXISTS permission; 9 | DROP TABLE IF EXISTS object; 10 | DROP TABLE IF EXISTS context; 11 | DROP TABLE IF EXISTS warrant; 12 | DROP TABLE IF EXISTS object_type; 13 | DROP FUNCTION IF EXISTS update_updated_at; 14 | 15 | COMMIT; 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000004_add_policy_column_to_warrant_table.up.sql: -------------------------------------------------------------------------------- 1 | -- NOTE: This migration is not fully reversible. 2 | -- It will drop all existing context data in favor of policies. 3 | DROP INDEX IF EXISTS warrant_uk_obj_rel_sub_ctx_hash; 4 | DROP TABLE IF EXISTS context; 5 | 6 | ALTER TABLE warrant 7 | ADD COLUMN policy text NOT NULL DEFAULT ''; 8 | 9 | ALTER TABLE warrant 10 | ADD COLUMN policyHash varchar(64) NOT NULL DEFAULT ''; 11 | 12 | -- NOTE: All existing warrants with context will be deleted. 13 | DELETE FROM warrant 14 | WHERE contextHash != ""; 15 | 16 | ALTER TABLE warrant 17 | DROP COLUMN contextHash; 18 | 19 | CREATE UNIQUE INDEX IF NOT EXISTS warrant_uk_obj_rel_sub_policy_hash 20 | ON warrant (objectType, objectId, relation, subjectType, subjectId, subjectRelation, policyHash); 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2024 WorkOS, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM alpine:3.17.2 16 | 17 | RUN addgroup -S warrant && adduser -S warrant -G warrant 18 | USER warrant 19 | 20 | WORKDIR ./ 21 | COPY ./warrant ./ 22 | 23 | ENTRYPOINT ["./warrant"] 24 | 25 | EXPOSE 8000 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up QEMU 12 | uses: docker/setup-qemu-action@v3 13 | with: 14 | platforms: amd64,arm64 15 | - name: Setup Go env 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: "^1.23.0" 19 | - name: Docker login 20 | uses: docker/login-action@v3 21 | with: 22 | username: ${{ secrets.DOCKER_USER }} 23 | password: ${{ secrets.DOCKER_TOKEN }} 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | - name: Release with Goreleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /pkg/database/database.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package database 16 | 17 | import "context" 18 | 19 | const ( 20 | TypeMySQL = "mysql" 21 | TypePostgres = "postgres" 22 | TypeSQLite = "sqlite" 23 | ) 24 | 25 | type Database interface { 26 | Type() string 27 | Connect(ctx context.Context) error 28 | Migrate(ctx context.Context, toVersion uint) error 29 | Ping(ctx context.Context) error 30 | WithinTransaction(ctx context.Context, txCallback func(ctx context.Context) error) error 31 | } 32 | -------------------------------------------------------------------------------- /pkg/service/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import "github.com/warrant-dev/warrant/pkg/database" 18 | 19 | type Env interface { 20 | DB() database.Database 21 | } 22 | 23 | type Service interface { 24 | Routes() ([]Route, error) 25 | Env() Env 26 | } 27 | 28 | type BaseService struct { 29 | env Env 30 | } 31 | 32 | func (svc BaseService) Env() Env { 33 | return svc.env 34 | } 35 | 36 | func NewBaseService(env Env) BaseService { 37 | return BaseService{ 38 | env: env, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000004_add_policy_column_to_warrant_table.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | # NOTE: This migration is not fully reversible. 4 | # It will drop all existing context data in favor of policies. 5 | 6 | ALTER TABLE warrant 7 | ADD COLUMN policy TEXT NOT NULL AFTER subjectRelation, 8 | ADD COLUMN policyHash VARCHAR(64) NOT NULL AFTER policy; 9 | 10 | # All existing context can be converted into policies that return the 11 | # intersection (&&) of a strict equality comparison with each context value. 12 | UPDATE warrant 13 | SET 14 | warrant.policy = ( 15 | SELECT GROUP_CONCAT(CONCAT(context.name, ' == ', '"', context.value, '"') SEPARATOR ' && ') 16 | FROM context 17 | WHERE context.warrantId = warrant.id 18 | ), 19 | warrant.policyHash = SHA2(warrant.policy, 256) 20 | WHERE warrant.contextHash != ""; 21 | 22 | ALTER TABLE warrant 23 | DROP INDEX warrant_uk_obj_rel_sub_ctx_hash, 24 | DROP COLUMN contextHash, 25 | ADD UNIQUE KEY warrant_uk_obj_rel_sub_policy_hash (objectType, objectId, relation, subjectType, subjectId, subjectRelation, policyHash); 26 | 27 | DROP TABLE IF EXISTS context; 28 | 29 | COMMIT; 30 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000004_add_policy_column_to_warrant_table.down.sql: -------------------------------------------------------------------------------- 1 | -- NOTE: Running this down migration will result in the loss 2 | -- of all warrant policies. 3 | CREATE TABLE IF NOT EXISTS context ( 4 | id INTEGER PRIMARY KEY AUTOINCREMENT, 5 | warrantId INTEGER NOT NULL, 6 | name TEXT NOT NULL, 7 | value TEXT NOT NULL, 8 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, 9 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, 10 | deletedAt DATETIME DEFAULT NULL, 11 | FOREIGN KEY (warrantId) REFERENCES warrant (id) 12 | ); 13 | 14 | CREATE UNIQUE INDEX IF NOT EXISTS context_uk_warrant_id_name 15 | ON context (warrantId, name); 16 | 17 | DELETE FROM warrant 18 | WHERE policy != ""; 19 | 20 | ALTER TABLE warrant 21 | ADD COLUMN contextHash TEXT NOT NULL DEFAULT ""; 22 | 23 | DROP INDEX IF EXISTS warrant_uk_obj_rel_sub_policy_hash; 24 | 25 | ALTER TABLE warrant 26 | DROP COLUMN policy; 27 | 28 | ALTER TABLE warrant 29 | DROP COLUMN policyHash; 30 | 31 | CREATE UNIQUE INDEX IF NOT EXISTS warrant_uk_obj_rel_sub_ctx_hash 32 | ON warrant (objectType, objectId, relation, subjectType, subjectId, subjectRelation, contextHash); 33 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000004_add_policy_column_to_warrant_table.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | -- NOTE: This migration is not fully reversible. 4 | -- It will drop all existing context data in favor of policies. 5 | ALTER TABLE warrant 6 | ADD COLUMN policy text NOT NULL DEFAULT '', 7 | ADD COLUMN policy_hash varchar(64) NOT NULL DEFAULT ''; 8 | 9 | -- All existing context can be converted into policies that return the 10 | -- intersection (&&) of a strict equality comparison with each context value. 11 | UPDATE warrant w 12 | SET 13 | policy = ( 14 | SELECT STRING_AGG(CONCAT(name, ' == ', '"', value, '"'), ' && ') 15 | FROM context 16 | WHERE warrant_id = w.id 17 | ) 18 | WHERE context_hash != ''; 19 | 20 | UPDATE warrant 21 | SET policy_hash = encode(sha256(policy::bytea), 'hex') 22 | WHERE policy != ''; 23 | 24 | ALTER TABLE warrant 25 | DROP CONSTRAINT warrant_uk_obj_rel_sub_ctx_hash, 26 | DROP COLUMN context_hash, 27 | ADD CONSTRAINT warrant_uk_obj_rel_sub_policy_hash UNIQUE (object_type, object_id, relation, subject_type, subject_id, subject_relation, policy_hash); 28 | 29 | DROP TABLE IF EXISTS context; 30 | 31 | COMMIT; 32 | -------------------------------------------------------------------------------- /pkg/wookie/wookie_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package wookie_test 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/warrant-dev/warrant/pkg/wookie" 22 | ) 23 | 24 | func TestBasicSerialization(t *testing.T) { 25 | t.Parallel() 26 | ctx := wookie.WithLatest(context.Background()) 27 | if !wookie.ContainsLatest(ctx) { 28 | t.Fatalf("expected ctx to contain 'latest' wookie") 29 | } 30 | 31 | ctx = context.Background() 32 | if wookie.ContainsLatest(ctx) { 33 | t.Fatalf("expected ctx to not contain 'latest' wookie") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/service/middleware.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "net/http" 19 | ) 20 | 21 | // Middleware defines the type of all middleware 22 | type Middleware func(http.Handler) http.Handler 23 | 24 | // ChainMiddleware a top-level middleware which applies the given middlewares in order from inner to outer (order of execution) 25 | func ChainMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler { 26 | for i := len(middlewares) - 1; i >= 0; i-- { 27 | handler = middlewares[i](handler) 28 | } 29 | return handler 30 | } 31 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000004_add_policy_column_to_warrant_table.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | -- NOTE: Running this down migration will result in the loss 4 | -- of all existing warrants with policies. 5 | CREATE TABLE IF NOT EXISTS context ( 6 | id bigserial PRIMARY KEY, 7 | warrant_id bigint NOT NULL REFERENCES warrant (id), 8 | name varchar(64) NOT NULL, 9 | value varchar(64) NOT NULL, 10 | created_at timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6), 11 | updated_at timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6), 12 | deleted_at timestamp(6) NULL DEFAULT NULL, 13 | CONSTRAINT context_uk_warrant_id_name UNIQUE (warrant_id, name) 14 | ); 15 | 16 | CREATE TRIGGER update_updated_at 17 | BEFORE UPDATE ON context 18 | FOR EACH ROW EXECUTE PROCEDURE update_updated_at(); 19 | 20 | DELETE FROM warrant 21 | WHERE policy != ''; 22 | 23 | ALTER TABLE warrant 24 | ADD COLUMN context_hash varchar(40) NOT NULL DEFAULT '', 25 | DROP CONSTRAINT warrant_uk_obj_rel_sub_policy_hash, 26 | DROP COLUMN policy, 27 | DROP COLUMN policy_hash, 28 | ADD CONSTRAINT warrant_uk_obj_rel_sub_ctx_hash UNIQUE (object_type, object_id, relation, subject_type, subject_id, subject_relation, context_hash); 29 | 30 | COMMIT; 31 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000004_add_policy_column_to_warrant_table.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | # NOTE: Running this down migration will result in the loss 4 | # of all existing warrant policies. 5 | CREATE TABLE IF NOT EXISTS context ( 6 | id int NOT NULL AUTO_INCREMENT, 7 | warrantId int NOT NULL, 8 | name varchar(64) NOT NULL, 9 | value varchar(64) NOT NULL, 10 | createdAt timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6), 11 | updatedAt timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), 12 | deletedAt timestamp(6) NULL DEFAULT NULL, 13 | PRIMARY KEY (id), 14 | UNIQUE KEY context_uk_warrant_id_name (warrantId, name), 15 | CONSTRAINT context_fk_warrant_id FOREIGN KEY (warrantId) REFERENCES warrant (id) 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 17 | 18 | DELETE FROM warrant 19 | WHERE policy != ""; 20 | 21 | ALTER TABLE warrant 22 | ADD COLUMN contextHash varchar(40) NOT NULL AFTER subjectRelation, 23 | DROP INDEX warrant_uk_obj_rel_sub_policy_hash, 24 | DROP COLUMN policy, 25 | DROP COLUMN policyHash; 26 | 27 | CREATE INDEX warrant_uk_obj_rel_sub_ctx_hash ON warrant(objectType, objectId, relation, subjectType, subjectId, subjectRelation, contextHash); 28 | 29 | COMMIT; 30 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000002_create_default_obj_types.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO objectType (typeId, definition) 2 | VALUES 3 | ('role', '{"type": "role", "relations": {"member": {"inheritIf": "member", "ofType": "role", "withRelation": "member"}}}'), 4 | ('permission', '{"type": "permission", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "permission", "withRelation": "member"}, {"inheritIf": "member", "ofType": "role", "withRelation": "member"}]}}}'), 5 | ('tenant', '{"type": "tenant", "relations": {"admin": {}, "member": {"inheritIf": "manager"}, "manager": {"inheritIf": "admin"}}}'), 6 | ('user', '{"type": "user", "relations": {"parent": {"inheritIf": "parent", "ofType": "user", "withRelation": "parent"}}}'), 7 | ('pricing-tier', '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'), 8 | ('feature', '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}') 9 | ON CONFLICT (typeId) DO UPDATE SET 10 | definition=excluded.definition, 11 | deletedAt=NULL; 12 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000002_create_default_obj_types.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | INSERT INTO objectType (typeId, definition) 4 | VALUES 5 | ('role', '{"type": "role", "relations": {"member": {"inheritIf": "member", "ofType": "role", "withRelation": "member"}}}'), 6 | ('permission', '{"type": "permission", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "permission", "withRelation": "member"}, {"inheritIf": "member", "ofType": "role", "withRelation": "member"}]}}}'), 7 | ('tenant', '{"type": "tenant", "relations": {"admin": {}, "member": {"inheritIf": "manager"}, "manager": {"inheritIf": "admin"}}}'), 8 | ('user', '{"type": "user", "relations": {"parent": {"inheritIf": "parent", "ofType": "user", "withRelation": "parent"}}}'), 9 | ('pricing-tier', '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'), 10 | ('feature', '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}') 11 | ON DUPLICATE KEY UPDATE 12 | definition = VALUES(definition), 13 | deletedAt = NULL; 14 | 15 | COMMIT; 16 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000002_create_default_obj_types.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | INSERT INTO object_type (type_id, definition) 4 | VALUES 5 | ('role', '{"type": "role", "relations": {"member": {"inheritIf": "member", "ofType": "role", "withRelation": "member"}}}'), 6 | ('permission', '{"type": "permission", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "permission", "withRelation": "member"}, {"inheritIf": "member", "ofType": "role", "withRelation": "member"}]}}}'), 7 | ('tenant', '{"type": "tenant", "relations": {"admin": {}, "member": {"inheritIf": "manager"}, "manager": {"inheritIf": "admin"}}}'), 8 | ('user', '{"type": "user", "relations": {"parent": {"inheritIf": "parent", "ofType": "user", "withRelation": "parent"}}}'), 9 | ('pricing-tier', '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'), 10 | ('feature', '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}') 11 | ON CONFLICT (type_id) DO UPDATE SET 12 | definition = EXCLUDED.definition, 13 | deleted_at = NULL; 14 | 15 | COMMIT; 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: "Go" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Setup Go env 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: "^1.23.0" 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 2 23 | - name: Verify Go dependencies 24 | run: go mod verify 25 | - name: Run unit tests 26 | run: go test -v ./... 27 | - name: Goreleaser check 28 | uses: goreleaser/goreleaser-action@v6 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: check 33 | golangci: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Setup Go env 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: "^1.23.0" 40 | cache: false 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 45 | - name: Run golangci-lint 46 | uses: golangci/golangci-lint-action@v6 47 | with: 48 | version: v1.60.3 49 | args: -v --timeout=5m 50 | only-new-issues: false 51 | install-mode: "binary" 52 | -------------------------------------------------------------------------------- /pkg/database/sqlite-stub.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !sqlite 16 | // +build !sqlite 17 | 18 | package database 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/pkg/errors" 24 | "github.com/warrant-dev/warrant/pkg/config" 25 | ) 26 | 27 | type SQLite struct { 28 | SQL 29 | Config config.SQLiteConfig 30 | } 31 | 32 | func NewSQLite(config config.SQLiteConfig) *SQLite { 33 | return nil 34 | } 35 | 36 | func (ds SQLite) Type() string { 37 | return TypeSQLite 38 | } 39 | 40 | func (ds *SQLite) Connect(ctx context.Context) error { 41 | return errors.New("sqlite not supported") 42 | } 43 | 44 | func (ds SQLite) Migrate(ctx context.Context, toVersion uint) error { 45 | return errors.New("sqlite not supported") 46 | } 47 | 48 | func (ds SQLite) Ping(ctx context.Context) error { 49 | return errors.New("sqlite not supported") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/service/route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import "net/http" 18 | 19 | type Route interface { 20 | GetPattern() string 21 | GetMethod() string 22 | GetHandler() http.Handler 23 | GetOverrideAuthMiddlewareFunc() AuthMiddlewareFunc 24 | } 25 | 26 | type WarrantRoute struct { 27 | Pattern string 28 | Method string 29 | Handler http.Handler 30 | OverrideAuthMiddlewareFunc AuthMiddlewareFunc 31 | } 32 | 33 | func (route WarrantRoute) GetPattern() string { 34 | return route.Pattern 35 | } 36 | 37 | func (route WarrantRoute) GetMethod() string { 38 | return route.Method 39 | } 40 | 41 | func (route WarrantRoute) GetHandler() http.Handler { 42 | return route.Handler 43 | } 44 | 45 | func (route WarrantRoute) GetOverrideAuthMiddlewareFunc() AuthMiddlewareFunc { 46 | return route.OverrideAuthMiddlewareFunc 47 | } 48 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000003_update_default_pricing_tier_feature_object_types.up.sql: -------------------------------------------------------------------------------- 1 | -- Update the default pricing-tier object type 2 | WITH 3 | currentDefaultPricingTierDefinition AS (SELECT '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}' AS value), 4 | newDefaultPricingTierDefinition AS (SELECT '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' AS value) 5 | UPDATE objectType 6 | SET definition = (SELECT value FROM newDefaultPricingTierDefinition) 7 | WHERE 8 | typeId = 'pricing-tier' AND 9 | definition = (SELECT value FROM currentDefaultPricingTierDefinition); 10 | 11 | -- Update the default feature object type 12 | WITH 13 | currentDefaultFeatureDefinition AS (SELECT '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}' AS value), 14 | newDefaultFeatureDefinition AS (SELECT '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' AS value) 15 | UPDATE objectType 16 | SET definition = (SELECT value FROM newDefaultFeatureDefinition) 17 | WHERE 18 | typeId = 'feature' AND 19 | definition = (SELECT value FROM currentDefaultFeatureDefinition); 20 | -------------------------------------------------------------------------------- /pkg/authz/objecttype/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | const PrimarySortKey = "typeId" 25 | 26 | type ObjectTypeListParamParser struct{} 27 | 28 | func (parser ObjectTypeListParamParser) GetDefaultSortBy() string { 29 | return "typeId" 30 | } 31 | 32 | func (parser ObjectTypeListParamParser) GetSupportedSortBys() []string { 33 | return []string{"createdAt", "typeId"} 34 | } 35 | 36 | func (parser ObjectTypeListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 37 | switch sortBy { 38 | //nolint:goconst 39 | case "createdAt": 40 | value, err := time.Parse(time.RFC3339, val) 41 | if err != nil || value.Equal(time.Time{}) { 42 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 43 | } 44 | 45 | return &value, nil 46 | case "typeId": 47 | if val == "" { 48 | return nil, errors.New("must not be empty") 49 | } 50 | 51 | return val, nil 52 | default: 53 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/000003_update_default_pricing_tier_feature_object_types.down.sql: -------------------------------------------------------------------------------- 1 | -- Update default pricing tier object type to old format 2 | WITH 3 | currentDefaultPricingTierDefinition AS (SELECT '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' AS value), 4 | oldDefaultPricingTierDefinition AS (SELECT '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}' AS value) 5 | UPDATE objectType 6 | SET definition = (SELECT value FROM oldDefaultPricingTierDefinition) 7 | WHERE 8 | typeId = 'pricing-tier' AND 9 | definition = (SELECT value FROM currentDefaultPricingTierDefinition); 10 | 11 | -- Update default feature object type to old format 12 | WITH 13 | currentDefaultFeatureDefinition AS (SELECT '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' AS value), 14 | oldDefaultFeatureDefinition AS (SELECT '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}' AS value) 15 | UPDATE objectType 16 | SET definition = (SELECT value FROM oldDefaultFeatureDefinition) 17 | WHERE 18 | typeId = 'feature' AND 19 | definition = (SELECT value FROM currentDefaultFeatureDefinition); 20 | -------------------------------------------------------------------------------- /pkg/wookie/wookie.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package wookie 16 | 17 | import ( 18 | "context" 19 | "net/http" 20 | ) 21 | 22 | const HeaderName = "Warrant-Token" 23 | const Latest = "latest" 24 | 25 | type warrantTokenCtxKey struct{} 26 | 27 | func WarrantTokenMiddleware(next http.Handler) http.Handler { 28 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 | headerVal := r.Header.Get(HeaderName) 30 | if headerVal != "" { 31 | warrantTokenCtx := context.WithValue(r.Context(), warrantTokenCtxKey{}, headerVal) 32 | next.ServeHTTP(w, r.WithContext(warrantTokenCtx)) 33 | return 34 | } 35 | next.ServeHTTP(w, r) 36 | }) 37 | } 38 | 39 | // Returns true if ctx contains wookie set to 'latest', false otherwise. 40 | func ContainsLatest(ctx context.Context) bool { 41 | if val, ok := ctx.Value(warrantTokenCtxKey{}).(string); ok { 42 | if val == Latest { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | // Return a context with Warrant-Token set to 'latest'. 50 | func WithLatest(parent context.Context) context.Context { 51 | return context.WithValue(parent, warrantTokenCtxKey{}, Latest) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/object/role/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type RoleListParamParser struct{} 25 | 26 | func (parser RoleListParamParser) GetDefaultSortBy() string { 27 | return "roleId" 28 | } 29 | 30 | func (parser RoleListParamParser) GetSupportedSortBys() []string { 31 | return []string{"createdAt", "roleId", "name"} 32 | } 33 | 34 | func (parser RoleListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 35 | switch sortBy { 36 | case "createdAt": 37 | value, err := time.Parse(time.RFC3339, val) 38 | if err != nil || value.Equal(time.Time{}) { 39 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 40 | } 41 | 42 | return &value, nil 43 | case "roleId": 44 | if val == "" { 45 | return nil, errors.New("must not be empty") 46 | } 47 | 48 | return val, nil 49 | case "name": 50 | if val == "" { 51 | return "", nil 52 | } 53 | 54 | return val, nil 55 | default: 56 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000003_update_default_pricing_tier_feature_object_types.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | -- Update the default pricing-tier object type to new format 4 | UPDATE object_type 5 | SET definition = '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' 6 | WHERE 7 | type_id = 'pricing-tier' AND 8 | definition @> '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'::jsonb AND 9 | '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'::jsonb @> definition; 10 | 11 | -- Update the default feature object type to new format 12 | UPDATE object_type 13 | SET definition = '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}' 14 | WHERE 15 | type_id = 'feature' AND 16 | definition @> '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}'::jsonb AND 17 | '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}'::jsonb @> definition; 18 | 19 | COMMIT; 20 | -------------------------------------------------------------------------------- /pkg/object/tenant/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tenant 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type TenantListParamParser struct{} 25 | 26 | func (parser TenantListParamParser) GetDefaultSortBy() string { 27 | return "tenantId" 28 | } 29 | 30 | func (parser TenantListParamParser) GetSupportedSortBys() []string { 31 | return []string{"createdAt", "name", "tenantId"} 32 | } 33 | 34 | func (parser TenantListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 35 | switch sortBy { 36 | case "createdAt": 37 | value, err := time.Parse(time.RFC3339, val) 38 | if err != nil || value.Equal(time.Time{}) { 39 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 40 | } 41 | 42 | return &value, nil 43 | case "name": 44 | if val == "" { 45 | return "", nil 46 | } 47 | 48 | return val, nil 49 | case "tenantId": 50 | if val == "" { 51 | return nil, errors.New("must not be empty") 52 | } 53 | 54 | return val, nil 55 | default: 56 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/object/feature/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type FeatureListParamParser struct{} 25 | 26 | func (parser FeatureListParamParser) GetDefaultSortBy() string { 27 | return "featureId" 28 | } 29 | 30 | func (parser FeatureListParamParser) GetSupportedSortBys() []string { 31 | return []string{"createdAt", "featureId", "name"} 32 | } 33 | 34 | func (parser FeatureListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 35 | switch sortBy { 36 | case "createdAt": 37 | value, err := time.Parse(time.RFC3339, val) 38 | if err != nil || value.Equal(time.Time{}) { 39 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 40 | } 41 | 42 | return &value, nil 43 | case "featureId": 44 | if val == "" { 45 | return nil, errors.New("must not be empty") 46 | } 47 | 48 | return val, nil 49 | case "name": 50 | if val == "" { 51 | return "", nil 52 | } 53 | 54 | return val, nil 55 | default: 56 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/object/permission/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type PermissionListParamParser struct{} 25 | 26 | func (parser PermissionListParamParser) GetDefaultSortBy() string { 27 | return "permissionId" 28 | } 29 | 30 | func (parser PermissionListParamParser) GetSupportedSortBys() []string { 31 | return []string{"createdAt", "permissionId", "name"} 32 | } 33 | 34 | func (parser PermissionListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 35 | switch sortBy { 36 | case "createdAt": 37 | value, err := time.Parse(time.RFC3339, val) 38 | if err != nil || value.Equal(time.Time{}) { 39 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 40 | } 41 | 42 | return &value, nil 43 | case "permissionId": 44 | if val == "" { 45 | return nil, errors.New("must not be empty") 46 | } 47 | 48 | return val, nil 49 | case "name": 50 | if val == "" { 51 | return "", nil 52 | } 53 | 54 | return val, nil 55 | default: 56 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000003_update_default_pricing_tier_feature_object_types.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET @currentDefaultPricingTierDefinition := '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'; 4 | SET @oldDefaultPricingTierDefinition := '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'; 5 | 6 | SET @currentDefaultFeatureDefinition := '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'; 7 | SET @oldDefaultFeatureDefinition := '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}'; 8 | 9 | # Update default pricing tier object type to old format 10 | UPDATE objectType 11 | SET definition = @oldDefaultPricingTierDefinition 12 | WHERE 13 | typeId = "pricing-tier" AND 14 | JSON_CONTAINS(definition, @currentDefaultPricingTierDefinition) AND 15 | JSON_CONTAINS(@currentDefaultPricingTierDefinition, definition); 16 | 17 | # Update default feature object type to old format 18 | UPDATE objectType 19 | SET definition = @oldDefaultFeatureDefinition 20 | WHERE 21 | typeId = "feature" AND 22 | JSON_CONTAINS(definition, @currentDefaultFeatureDefinition) AND 23 | JSON_CONTAINS(@currentDefaultFeatureDefinition, definition); 24 | 25 | COMMIT; 26 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/000003_update_default_pricing_tier_feature_object_types.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET @currentDefaultPricingTierDefinition := '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}'; 4 | SET @newDefaultPricingTierDefinition := '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'; 5 | 6 | SET @currentDefaultFeatureDefinition := '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}'; 7 | SET @newDefaultFeatureDefinition := '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'; 8 | 9 | # Update the default pricing-tier object type to new format 10 | UPDATE objectType 11 | SET definition = @newDefaultPricingTierDefinition 12 | WHERE 13 | typeId = "pricing-tier" AND 14 | JSON_CONTAINS(definition, @currentDefaultPricingTierDefinition) AND 15 | JSON_CONTAINS(@currentDefaultPricingTierDefinition, definition); 16 | 17 | # Update the default feature object type to new format 18 | UPDATE objectType 19 | SET definition = @newDefaultFeatureDefinition 20 | WHERE 21 | typeId = "feature" AND 22 | JSON_CONTAINS(definition, @currentDefaultFeatureDefinition) AND 23 | JSON_CONTAINS(@currentDefaultFeatureDefinition, definition); 24 | 25 | COMMIT; 26 | -------------------------------------------------------------------------------- /pkg/object/pricingtier/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type PricingTierListParamParser struct{} 25 | 26 | func (parser PricingTierListParamParser) GetDefaultSortBy() string { 27 | return "pricingTierId" 28 | } 29 | 30 | func (parser PricingTierListParamParser) GetSupportedSortBys() []string { 31 | return []string{"createdAt", "pricingTierId", "name"} 32 | } 33 | 34 | func (parser PricingTierListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 35 | switch sortBy { 36 | case "createdAt": 37 | value, err := time.Parse(time.RFC3339, val) 38 | if err != nil || value.Equal(time.Time{}) { 39 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 40 | } 41 | 42 | return &value, nil 43 | case "pricingTierId": 44 | if val == "" { 45 | return nil, errors.New("must not be empty") 46 | } 47 | 48 | return val, nil 49 | 50 | case "name": 51 | if val == "" { 52 | return "", nil 53 | } 54 | 55 | return val, nil 56 | default: 57 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/object/user/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "net/mail" 20 | "time" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | type UserListParamParser struct{} 26 | 27 | func (parser UserListParamParser) GetDefaultSortBy() string { 28 | return "userId" 29 | } 30 | 31 | func (parser UserListParamParser) GetSupportedSortBys() []string { 32 | return []string{"createdAt", "userId", "email"} 33 | } 34 | 35 | func (parser UserListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 36 | switch sortBy { 37 | case "createdAt": 38 | value, err := time.Parse(time.RFC3339, val) 39 | if err != nil || value.Equal(time.Time{}) { 40 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 41 | } 42 | 43 | return &value, nil 44 | case "email": 45 | if val == "" { 46 | return "", nil 47 | } 48 | 49 | afterValue, err := mail.ParseAddress(val) 50 | if err != nil { 51 | return nil, errors.New("must be a valid email") 52 | } 53 | 54 | return afterValue.Address, nil 55 | case "userId": 56 | if val == "" { 57 | return nil, errors.New("must not be empty") 58 | } 59 | 60 | return val, nil 61 | default: 62 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000003_update_default_pricing_tier_feature_object_types.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | -- Update default pricing tier object type to old format 4 | UPDATE object_type 5 | SET definition = '{"type": "pricing-tier", "relations": {"member": {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}}}' 6 | WHERE 7 | type_id = 'pricing-tier' AND 8 | definition @> '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'::jsonb AND 9 | '{"type": "pricing-tier", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "pricing-tier", "withRelation": "member"}, {"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'::jsonb @> definition; 10 | 11 | -- Update default feature object type to old format 12 | UPDATE object_type 13 | SET definition = '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"}]}}}' 14 | WHERE 15 | type_id = 'feature' AND 16 | definition @> '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'::jsonb AND 17 | '{"type": "feature", "relations": {"member": {"inheritIf": "anyOf", "rules": [{"inheritIf": "member", "ofType": "feature", "withRelation": "member"}, {"ofType": "pricing-tier", "inheritIf": "member", "withRelation": "member"},{"inheritIf": "member", "ofType": "tenant", "withRelation": "member"}]}}}'::jsonb @> definition; 18 | 19 | COMMIT; 20 | -------------------------------------------------------------------------------- /pkg/object/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | const PrimarySortKey = "objectId" 25 | 26 | var supportedSortBys = []string{"objectId", "createdAt", "objectType"} 27 | 28 | type ObjectListParamParser struct{} 29 | 30 | func (parser ObjectListParamParser) GetDefaultSortBy() string { 31 | return "objectId" 32 | } 33 | 34 | func (parser ObjectListParamParser) GetSupportedSortBys() []string { 35 | return supportedSortBys 36 | } 37 | 38 | func (parser ObjectListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 39 | switch sortBy { 40 | case "createdAt": 41 | value, err := time.Parse(time.RFC3339, val) 42 | if err != nil || value.Equal(time.Time{}) { 43 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 44 | } 45 | 46 | return &value, nil 47 | case "objectType", "objectId": 48 | if val == "" { 49 | return nil, errors.New("must not be empty") 50 | } 51 | 52 | return val, nil 53 | default: 54 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 55 | } 56 | } 57 | 58 | func IsObjectSortBy(sortBy string) bool { 59 | for _, supportedSortBy := range supportedSortBys { 60 | if sortBy == supportedSortBy { 61 | return true 62 | } 63 | } 64 | 65 | return false 66 | } 67 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000005_update_timestamp_column_types.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE feature 4 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 5 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 6 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 7 | ALTER COLUMN deleted_at SET DEFAULT NULL; 8 | 9 | ALTER TABLE object 10 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 11 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 12 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 13 | ALTER COLUMN deleted_at SET DEFAULT NULL; 14 | 15 | ALTER TABLE object_type 16 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 17 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 18 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 19 | ALTER COLUMN deleted_at SET DEFAULT NULL; 20 | 21 | ALTER TABLE permission 22 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 23 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 24 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 25 | ALTER COLUMN deleted_at SET DEFAULT NULL; 26 | 27 | ALTER TABLE pricing_tier 28 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 29 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 30 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 31 | ALTER COLUMN deleted_at SET DEFAULT NULL; 32 | 33 | ALTER TABLE role 34 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 35 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 36 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 37 | ALTER COLUMN deleted_at SET DEFAULT NULL; 38 | 39 | ALTER TABLE tenant 40 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 41 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 42 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 43 | ALTER COLUMN deleted_at SET DEFAULT NULL; 44 | 45 | ALTER TABLE "user" 46 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 47 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 48 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 49 | ALTER COLUMN deleted_at SET DEFAULT NULL; 50 | 51 | ALTER TABLE warrant 52 | ALTER COLUMN created_at TYPE TIMESTAMP(6), 53 | ALTER COLUMN updated_at TYPE TIMESTAMP(6), 54 | ALTER COLUMN deleted_at TYPE TIMESTAMP(6), 55 | ALTER COLUMN deleted_at SET DEFAULT NULL; 56 | 57 | COMMIT; 58 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/000005_update_timestamp_column_types.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE feature 4 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 5 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 6 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 7 | ALTER COLUMN deleted_at SET DEFAULT NULL; 8 | 9 | ALTER TABLE object 10 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 11 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 12 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 13 | ALTER COLUMN deleted_at SET DEFAULT NULL; 14 | 15 | ALTER TABLE object_type 16 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 17 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 18 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 19 | ALTER COLUMN deleted_at SET DEFAULT NULL; 20 | 21 | ALTER TABLE permission 22 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 23 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 24 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 25 | ALTER COLUMN deleted_at SET DEFAULT NULL; 26 | 27 | ALTER TABLE pricing_tier 28 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 29 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 30 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 31 | ALTER COLUMN deleted_at SET DEFAULT NULL; 32 | 33 | ALTER TABLE role 34 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 35 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 36 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 37 | ALTER COLUMN deleted_at SET DEFAULT NULL; 38 | 39 | ALTER TABLE tenant 40 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 41 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 42 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 43 | ALTER COLUMN deleted_at SET DEFAULT NULL; 44 | 45 | ALTER TABLE "user" 46 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 47 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 48 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 49 | ALTER COLUMN deleted_at SET DEFAULT NULL; 50 | 51 | ALTER TABLE warrant 52 | ALTER COLUMN created_at TYPE TIMESTAMPTZ(6), 53 | ALTER COLUMN updated_at TYPE TIMESTAMPTZ(6), 54 | ALTER COLUMN deleted_at TYPE TIMESTAMPTZ(6), 55 | ALTER COLUMN deleted_at SET DEFAULT NULL; 56 | 57 | COMMIT; 58 | -------------------------------------------------------------------------------- /migrations/datastore/sqlite/README.md: -------------------------------------------------------------------------------- 1 | # Running Warrant with SQLite 2 | 3 | This guide covers how to set up SQLite as a datastore for Warrant. 4 | 5 | Note: Please first refer to the [development guide](/development.md) to ensure that your Go environment is set up and you have checked out the Warrant source or [downloaded a binary](https://github.com/warrant-dev/warrant/releases). 6 | 7 | ## Install SQLite 8 | 9 | Many operating systems (like MacOS) come with SQLite pre-installed. If you already have SQLite installed, you can skip to the next step. If you don't already have SQLite installed, [install it](https://www.tutorialspoint.com/sqlite/sqlite_installation.htm). Once installed, you should be able to run the following command to print the currently installed version of SQLite: 10 | 11 | ```bash 12 | sqlite3 --version 13 | ``` 14 | 15 | ## Warrant configuration 16 | 17 | The Warrant server requires certain configuration, defined either within a `warrant.yaml` file (located within the same directory as the binary) or via environment variables. This configuration includes some common variables and some SQLite specific variables. Here's a sample config: 18 | 19 | ### Sample `warrant.yaml` config 20 | 21 | ```yaml 22 | port: 8000 23 | logLevel: 1 24 | enableAccessLog: true 25 | autoMigrate: true 26 | authentication: 27 | apiKey: replace_with_api_key 28 | datastore: 29 | sqlite: 30 | database: warrant 31 | inMemory: true 32 | ``` 33 | 34 | Note: By default, SQLite will create a database file for the datastore. The filename is configurable using the `database` property under `datastore`. Specifying the `inMemory` option under `datastore` will create the database file in memory and will not persist it to the filesystem. When running Warrant with the `inMemory` configuration, **any data in Warrant will be lost once the Warrant process is shutdown/killed**. 35 | 36 | Unlike `mysql` and `postgresql`, `sqlite` currently does not support manually running db migrations on the command line via golang-migrate. Therefore, you should keep `autoMigrate` set to true in your Warrant config so that the server runs migrations as part of startup. 37 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | # - go mod tidy 5 | # - go generate ./... 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | main: ./cmd/warrant/ 10 | binary: warrant 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | goarch: 16 | - amd64 17 | - arm64 18 | ldflags: 19 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 20 | archives: 21 | - format: tar.gz 22 | name_template: >- 23 | {{ .ProjectName }}_ 24 | {{- title .Os }}_ 25 | {{- if eq .Arch "amd64" }}x86_64 26 | {{- else if eq .Arch "386" }}i386 27 | {{- else }}{{ .Arch }}{{ end }} 28 | {{- if .Arm }}v{{ .Arm }}{{ end }} 29 | # use zip for windows archives 30 | format_overrides: 31 | - goos: windows 32 | format: zip 33 | checksum: 34 | name_template: 'checksums.txt' 35 | snapshot: 36 | version_template: "{{ incpatch .Version }}-next" 37 | changelog: 38 | sort: asc 39 | filters: 40 | exclude: 41 | - '^docs:' 42 | - '^test:' 43 | dockers: 44 | - id: warrant-amd64 45 | image_templates: 46 | - "warrantdev/warrant:{{ .Tag }}-amd64" 47 | - "warrantdev/warrant:latest-amd64" 48 | goos: linux 49 | goarch: amd64 50 | use: buildx 51 | skip_push: false 52 | dockerfile: "Dockerfile" 53 | build_flag_templates: 54 | - "--platform=linux/amd64" 55 | - id: warrant-arm64 56 | image_templates: 57 | - "warrantdev/warrant:{{ .Tag }}-arm64" 58 | - "warrantdev/warrant:latest-arm64" 59 | goos: linux 60 | goarch: arm64 61 | use: buildx 62 | skip_push: false 63 | dockerfile: "Dockerfile" 64 | build_flag_templates: 65 | - "--platform=linux/arm64" 66 | docker_manifests: 67 | - name_template: "warrantdev/warrant:{{ .Tag }}" 68 | image_templates: 69 | - "warrantdev/warrant:{{ .Tag }}-amd64" 70 | - "warrantdev/warrant:{{ .Tag }}-arm64" 71 | - name_template: "warrantdev/warrant:latest" 72 | image_templates: 73 | - "warrantdev/warrant:latest-amd64" 74 | - "warrantdev/warrant:latest-arm64" 75 | -------------------------------------------------------------------------------- /migrations/datastore/postgres/README.md: -------------------------------------------------------------------------------- 1 | # Running Warrant with PostgreSQL 2 | 3 | This guide covers how to set up PostgreSQL as a datastore for Warrant. 4 | 5 | Note: Please first refer to the [development guide](/development.md) to ensure that your Go environment is set up and you have checked out the Warrant source or [downloaded a binary](https://github.com/warrant-dev/warrant/releases). 6 | 7 | ## Install PostgreSQL 8 | 9 | Install and run the [PostgreSQL Installer](https://www.postgresql.org/download/) for your OS to install and start PostgreSQL. For MacOS users, we recommend [installing PostgreSQL using homebrew](https://formulae.brew.sh/formula/postgresql@14). 10 | 11 | ## Warrant configuration 12 | 13 | The Warrant server requires certain configuration, defined either within a `warrant.yaml` file (located within the same directory as the binary) or via environment variables. This configuration includes some common variables and some PostgreSQL specific variables. Here's a sample config: 14 | 15 | ### Sample `warrant.yaml` config 16 | 17 | ```yaml 18 | port: 8000 19 | logLevel: 1 20 | enableAccessLog: true 21 | autoMigrate: true 22 | authentication: 23 | apiKey: replace_with_api_key 24 | datastore: 25 | postgres: 26 | username: replace_with_username 27 | password: replace_with_password 28 | hostname: localhost 29 | database: warrant 30 | sslmode: disable 31 | ``` 32 | 33 | Note: You can create a databases via the postgres command line and configure it as the `database` attribute under `datastore`. 34 | 35 | ## Running db migrations 36 | 37 | Warrant uses [golang-migrate](https://github.com/golang-migrate/migrate) to manage sql db migrations. If the `autoMigrate` config flag is set to true, the server will automatically run migrations on start. If you prefer managing migrations and upgrades manually, please set the `autoMigrate` flag to false. 38 | 39 | You can [install golang-migrate yourself](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate) and run the PostgreSQL migrations manually: 40 | 41 | ```shell 42 | migrate -path ./migrations/datastore/postgres/ -database postgres://username:password@hostname/warrant up 43 | ``` 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Warrant is under active development and welcomes contributions from the community via pull requests. 4 | 5 | Prior to submitting any PRs, please review and familiarize yourself with the following guidelines and our [Code of Conduct](/CODE_OF_CONDUCT.md). 6 | 7 | ## Issues 8 | 9 | - All outstanding bugs and feature requests are [tracked as GitHub issues](https://github.com/warrant-dev/warrant/issues). 10 | - If you find a bug or have a feature request, please [open an issue](https://github.com/warrant-dev/warrant/issues/new/choose). In order to prevent duplicate reports, please first search through existing open issues for your request prior to creating a new one. 11 | - If you discover a security issue or vulnerability, do not create an issue. Please email us with details at security@workos.com. 12 | - If you find small mistakes or issues in docs/instructions etc., feel free to submit PR fixes without first creating issues. 13 | - If you'd like to contribute a fix or implementation for an issue, please first consult on your approach with a member of the Warrant team directly on the GitHub issue. 14 | - [Here](https://github.com/warrant-dev/warrant/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) is a list of 'good first issues' for new contributors. 15 | 16 | ## Making changes 17 | 18 | - Refer to the [local development guide](/development.md) to get your development environment set up. 19 | - Warrant uses the fork & pull request flow. Please make sure to fork your own copy of the repo and use feature branches for development. 20 | - Make sure to [test your changes](/development.md#running-tests) and add tests where necessary. 21 | 22 | ## Submitting pull requests 23 | 24 | - Unless it's a minor change, never submit a PR without an associated issue. 25 | - Once you've implemented and tested your code changes, submit a pull request. 26 | - Pull requests will trigger ci jobs that run linters, static analysis and tests. It is the submitter's responsibility to ensure that all ci checks are passing. 27 | - A member of the Warrant team will review your PR. Once approved, you may merge your PR into main. 28 | - New versions will be tagged and released automatically. 29 | -------------------------------------------------------------------------------- /pkg/wookie/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package wookie 16 | 17 | import ( 18 | "encoding/base64" 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | "time" 23 | 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | type Token struct { 28 | ID int64 29 | Version int64 30 | Timestamp time.Time 31 | } 32 | 33 | // Get string representation of token (to set as header). 34 | func (t Token) String() string { 35 | s := fmt.Sprintf("%d;%d;%d", t.ID, t.Version, t.Timestamp.UnixMicro()) 36 | return base64.StdEncoding.EncodeToString([]byte(s)) 37 | } 38 | 39 | // De-serialize token from string (from header). 40 | func FromString(wookieString string) (*Token, error) { 41 | if wookieString == "" { 42 | return nil, errors.New("empty wookie string") 43 | } 44 | decodedStr, err := base64.StdEncoding.DecodeString(wookieString) 45 | if err != nil { 46 | return nil, errors.New("invalid wookie string") 47 | } 48 | parts := strings.Split(string(decodedStr), ";") 49 | if len(parts) != 3 { 50 | return nil, errors.New("invalid wookie string") 51 | } 52 | id, err := strconv.ParseInt(parts[0], 0, 64) 53 | if err != nil { 54 | return nil, errors.New("invalid id in wookie string") 55 | } 56 | version, err := strconv.ParseInt(parts[1], 0, 64) 57 | if err != nil { 58 | return nil, errors.New("invalid version in wookie string") 59 | } 60 | microTs, err := strconv.ParseInt(parts[2], 0, 64) 61 | if err != nil { 62 | return nil, errors.New("invalid timestamp in wookie string") 63 | } 64 | timestamp := time.UnixMicro(microTs) 65 | 66 | return &Token{ 67 | ID: id, 68 | Version: version, 69 | Timestamp: timestamp, 70 | }, nil 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/sqlite.yaml: -------------------------------------------------------------------------------- 1 | name: SQLite 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup Go env 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "^1.23.0" 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 2 24 | - name: Build binary 25 | run: make dev 26 | working-directory: cmd/warrant 27 | - name: Install apirunner 28 | run: go install github.com/warrant-dev/apirunner/cmd/apirunner@latest 29 | - name: Start test server 30 | id: start-server 31 | run: ./cmd/warrant/bin/warrant > server.log 2>&1 & 32 | env: 33 | WARRANT_AUTOMIGRATE: true 34 | WARRANT_PORT: 8000 35 | WARRANT_LOGLEVEL: 0 36 | WARRANT_ENABLEACCESSLOG: true 37 | WARRANT_AUTHENTICATION_APIKEY: warrant_api_key 38 | WARRANT_CHECK_CONCURRENCY: 4 39 | WARRANT_CHECK_MAXCONCURRENCY: 1000 40 | WARRANT_CHECK_TIMEOUT: 1m 41 | WARRANT_DATASTORE: sqlite 42 | WARRANT_DATASTORE_SQLITE_DATABASE: warrant 43 | WARRANT_DATASTORE_SQLITE_INMEMORY: true 44 | WARRANT_DATASTORE_SQLITE_MIGRATIONSOURCE: file://./migrations/datastore/sqlite 45 | WARRANT_DATASTORE_SQLITE_MAXIDLECONNECTIONS: 1 46 | WARRANT_DATASTORE_SQLITE_MAXOPENCONNECTIONS: 1 47 | WARRANT_DATASTORE_SQLITE_CONNMAXIDLETIME: 4h 48 | WARRANT_DATASTORE_SQLITE_CONNMAXLIFETIME: 6h 49 | - name: Run apirunner tests 50 | run: | 51 | sleep 3 52 | apirunner tests/ '.*' tests/ci-apirunner.conf 53 | - name: Shutdown test server 54 | if: success() || (failure() && steps.start-server.outcome == 'success') 55 | run: kill -9 `lsof -i:8000 -t` 56 | - name: Archive server log 57 | if: failure() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: server-log 61 | path: server.log 62 | if-no-files-found: warn 63 | retention-days: 5 64 | -------------------------------------------------------------------------------- /pkg/authz/objecttype/repository.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/warrant-dev/warrant/pkg/database" 23 | "github.com/warrant-dev/warrant/pkg/service" 24 | ) 25 | 26 | type ObjectTypeRepository interface { 27 | Create(ctx context.Context, objectType Model) (int64, error) 28 | GetById(ctx context.Context, id int64) (Model, error) 29 | GetByTypeId(ctx context.Context, typeId string) (Model, error) 30 | List(ctx context.Context, listParams service.ListParams) ([]Model, *service.Cursor, *service.Cursor, error) 31 | UpdateByTypeId(ctx context.Context, typeId string, objectType Model) error 32 | DeleteByTypeId(ctx context.Context, typeId string) error 33 | } 34 | 35 | func NewRepository(db database.Database) (ObjectTypeRepository, error) { 36 | switch db.Type() { 37 | case database.TypeMySQL: 38 | mysql, ok := db.(*database.MySQL) 39 | if !ok { 40 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeMySQL)) 41 | } 42 | 43 | return NewMySQLRepository(mysql), nil 44 | case database.TypePostgres: 45 | postgres, ok := db.(*database.Postgres) 46 | if !ok { 47 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypePostgres)) 48 | } 49 | 50 | return NewPostgresRepository(postgres), nil 51 | case database.TypeSQLite: 52 | sqlite, ok := db.(*database.SQLite) 53 | if !ok { 54 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeSQLite)) 55 | } 56 | 57 | return NewSQLiteRepository(sqlite), nil 58 | default: 59 | return nil, errors.New(fmt.Sprintf("unsupported database type %s specified", db.Type())) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - migrations 4 | - tests 5 | linters: 6 | enable-all: true 7 | disable: 8 | # Deprecated: 9 | - deadcode 10 | - exhaustivestruct 11 | - golint 12 | - ifshort 13 | - interfacer 14 | - maligned 15 | - nosnakecase 16 | - scopelint 17 | - structcheck 18 | - varcheck 19 | 20 | # Should review/fix: 21 | # - cyclop 22 | - depguard 23 | - dupl 24 | - dupword 25 | # - errorlint 26 | # - exhaustive 27 | - exhaustruct 28 | # - forcetypeassert 29 | - funlen 30 | - gci 31 | - gochecknoglobals 32 | # - gochecknoinits 33 | # - gocognit 34 | # - gocritic 35 | # - gocyclo 36 | # - godot 37 | - godox 38 | - goerr113 39 | - gofumpt 40 | - gomnd 41 | # - gosec 42 | - interfacebloat 43 | - ireturn 44 | # - mirror 45 | # - nestif 46 | # - nilerr 47 | # - nilnil 48 | - nlreturn 49 | # - noctx 50 | # - nonamedreturns 51 | # - paralleltest 52 | # - reassign 53 | # Revive needs config: 54 | - revive 55 | - stylecheck 56 | # - tagalign 57 | # - testpackage 58 | # - unconvert 59 | - unparam 60 | - varnamelen 61 | - wrapcheck 62 | - wsl 63 | linters-settings: 64 | goheader: 65 | template: |- 66 | Copyright 2024 WorkOS, Inc. 67 | 68 | Licensed under the Apache License, Version 2.0 (the "License"); 69 | you may not use this file except in compliance with the License. 70 | You may obtain a copy of the License at 71 | 72 | http://www.apache.org/licenses/LICENSE-2.0 73 | 74 | Unless required by applicable law or agreed to in writing, software 75 | distributed under the License is distributed on an "AS IS" BASIS, 76 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 77 | See the License for the specific language governing permissions and 78 | limitations under the License. 79 | lll: 80 | line-length: 270 81 | nestif: 82 | min-complexity: 40 83 | cyclop: 84 | max-complexity: 100 85 | gocognit: 86 | min-complexity: 150 87 | gocyclo: 88 | min-complexity: 80 89 | maintidx: 90 | under: 10 91 | issues: 92 | new-from-rev: 578853fe0dd71f4b624746915d27a1eae57c2397 93 | max-issues-per-linter: 0 94 | max-same-issues: 0 95 | -------------------------------------------------------------------------------- /pkg/authz/warrant/subject.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type SubjectSpec struct { 25 | ObjectType string `json:"objectType,omitempty" validate:"required_with=ObjectId,valid_object_type"` 26 | ObjectId string `json:"objectId,omitempty" validate:"required_with=ObjectType,valid_object_id"` 27 | Relation string `json:"relation,omitempty" validate:"omitempty,valid_relation"` 28 | } 29 | 30 | func (spec *SubjectSpec) String() string { 31 | if spec.Relation != "" { 32 | return fmt.Sprintf("%s:%s#%s", spec.ObjectType, spec.ObjectId, spec.Relation) 33 | } 34 | 35 | return fmt.Sprintf("%s:%s", spec.ObjectType, spec.ObjectId) 36 | } 37 | 38 | func StringToSubjectSpec(str string) (*SubjectSpec, error) { 39 | objectAndRelation := strings.Split(str, "#") 40 | if len(objectAndRelation) > 2 { 41 | return nil, errors.New(fmt.Sprintf("invalid subject string %s", str)) 42 | } 43 | 44 | if len(objectAndRelation) == 1 { 45 | objectType, objectId, colonFound := strings.Cut(str, ":") 46 | 47 | if !colonFound { 48 | return nil, errors.New(fmt.Sprintf("invalid subject string %s", str)) 49 | } 50 | 51 | return &SubjectSpec{ 52 | ObjectType: objectType, 53 | ObjectId: objectId, 54 | }, nil 55 | } 56 | 57 | object := objectAndRelation[0] 58 | relation := objectAndRelation[1] 59 | 60 | objectType, objectId, colonFound := strings.Cut(object, ":") 61 | if !colonFound { 62 | return nil, errors.New(fmt.Sprintf("invalid subject string %s", str)) 63 | } 64 | 65 | subjectSpec := &SubjectSpec{ 66 | ObjectType: objectType, 67 | ObjectId: objectId, 68 | } 69 | if relation != "" { 70 | subjectSpec.Relation = relation 71 | } 72 | 73 | return subjectSpec, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/object/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "encoding/json" 19 | "time" 20 | 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | type FilterOptions struct { 25 | ObjectType string `json:"objectType,omitempty"` 26 | } 27 | 28 | type ObjectSpec struct { 29 | // NOTE: ID is required here for internal use. 30 | // However, we don't return it to the client. 31 | ID int64 `json:"-"` 32 | ObjectType string `json:"objectType"` 33 | ObjectId string `json:"objectId"` 34 | Meta map[string]interface{} `json:"meta,omitempty"` 35 | CreatedAt time.Time `json:"createdAt"` 36 | } 37 | 38 | type CreateObjectSpec struct { 39 | ObjectType string `json:"objectType" validate:"required,valid_object_type"` 40 | ObjectId string `json:"objectId" validate:"omitempty,valid_object_id"` 41 | Meta map[string]interface{} `json:"meta"` 42 | } 43 | 44 | func (spec CreateObjectSpec) ToObject() (*Object, error) { 45 | var meta *string 46 | if spec.Meta != nil { 47 | m, err := json.Marshal(spec.Meta) 48 | if err != nil { 49 | return nil, service.NewInvalidParameterError("meta", "invalid format") 50 | } 51 | 52 | metaStr := string(m) 53 | meta = &metaStr 54 | } 55 | 56 | return &Object{ 57 | ObjectType: spec.ObjectType, 58 | ObjectId: spec.ObjectId, 59 | Meta: meta, 60 | }, nil 61 | } 62 | 63 | type UpdateObjectSpec struct { 64 | Meta map[string]interface{} `json:"meta"` 65 | } 66 | 67 | type ListObjectSpecV1 []ObjectSpec 68 | 69 | type ListObjectsSpecV2 struct { 70 | Results []ObjectSpec `json:"results"` 71 | NextCursor *service.Cursor `json:"nextCursor,omitempty"` 72 | PrevCursor *service.Cursor `json:"prevCursor,omitempty"` 73 | } 74 | -------------------------------------------------------------------------------- /pkg/object/user/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type UserSpec struct { 26 | UserId string `json:"userId" validate:"omitempty,valid_object_id"` 27 | Email *string `json:"email"` 28 | CreatedAt time.Time `json:"createdAt"` 29 | } 30 | 31 | func NewUserSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*UserSpec, error) { 32 | var email *string 33 | 34 | if objectSpec.Meta != nil { 35 | if _, exists := objectSpec.Meta["email"]; exists { 36 | emailStr, ok := objectSpec.Meta["email"].(string) 37 | if !ok { 38 | return nil, errors.New("user email has invalid type in object meta") 39 | } 40 | email = &emailStr 41 | } 42 | } 43 | 44 | return &UserSpec{ 45 | UserId: objectSpec.ObjectId, 46 | Email: email, 47 | CreatedAt: objectSpec.CreatedAt, 48 | }, nil 49 | } 50 | 51 | func (spec UserSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 52 | createObjectSpec := object.CreateObjectSpec{ 53 | ObjectType: objecttype.ObjectTypeUser, 54 | ObjectId: spec.UserId, 55 | } 56 | 57 | meta := make(map[string]interface{}) 58 | if spec.Email != nil { 59 | meta["email"] = spec.Email 60 | } 61 | 62 | if len(meta) > 0 { 63 | createObjectSpec.Meta = meta 64 | } 65 | 66 | return &createObjectSpec, nil 67 | } 68 | 69 | type UpdateUserSpec struct { 70 | Email *string `json:"email"` 71 | } 72 | 73 | func (updateSpec UpdateUserSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 74 | meta := make(map[string]interface{}) 75 | 76 | if updateSpec.Email != nil { 77 | meta["email"] = updateSpec.Email 78 | } 79 | 80 | return &object.UpdateObjectSpec{ 81 | Meta: meta, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/object/tenant/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tenant 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type TenantSpec struct { 26 | TenantId string `json:"tenantId" validate:"omitempty,valid_object_id"` 27 | Name *string `json:"name"` 28 | CreatedAt time.Time `json:"createdAt"` 29 | } 30 | 31 | func NewTenantSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*TenantSpec, error) { 32 | var name *string 33 | 34 | if objectSpec.Meta != nil { 35 | if _, exists := objectSpec.Meta["name"]; exists { 36 | nameStr, ok := objectSpec.Meta["name"].(string) 37 | if !ok { 38 | return nil, errors.New("tenant name has invalid type in object meta") 39 | } 40 | name = &nameStr 41 | } 42 | } 43 | 44 | return &TenantSpec{ 45 | TenantId: objectSpec.ObjectId, 46 | Name: name, 47 | CreatedAt: objectSpec.CreatedAt, 48 | }, nil 49 | } 50 | 51 | func (spec TenantSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 52 | createObjectSpec := object.CreateObjectSpec{ 53 | ObjectType: objecttype.ObjectTypeTenant, 54 | ObjectId: spec.TenantId, 55 | } 56 | 57 | meta := make(map[string]interface{}) 58 | if spec.Name != nil { 59 | meta["name"] = spec.Name 60 | } 61 | 62 | if len(meta) > 0 { 63 | createObjectSpec.Meta = meta 64 | } 65 | 66 | return &createObjectSpec, nil 67 | } 68 | 69 | type UpdateTenantSpec struct { 70 | Name *string `json:"name"` 71 | } 72 | 73 | func (updateSpec UpdateTenantSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 74 | meta := make(map[string]interface{}) 75 | 76 | if updateSpec.Name != nil { 77 | meta["name"] = updateSpec.Name 78 | } 79 | 80 | return &object.UpdateObjectSpec{ 81 | Meta: meta, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/warrant-dev/warrant 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/alecthomas/participle/v2 v2.1.1 7 | github.com/antonmedv/expr v1.15.5 8 | github.com/go-playground/validator/v10 v10.24.0 9 | github.com/golang-jwt/jwt/v5 v5.2.1 10 | github.com/golang-migrate/migrate/v4 v4.18.1 11 | github.com/google/go-cmp v0.6.0 12 | github.com/google/uuid v1.6.0 13 | github.com/gorilla/mux v1.8.1 14 | github.com/jmoiron/sqlx v1.4.0 15 | github.com/mattn/go-sqlite3 v1.14.24 16 | github.com/pkg/errors v0.9.1 17 | github.com/rs/zerolog v1.33.0 18 | github.com/spf13/viper v1.19.0 19 | ) 20 | 21 | require ( 22 | filippo.io/edwards25519 v1.1.0 // indirect 23 | github.com/fsnotify/fsnotify v1.7.0 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-sql-driver/mysql v1.8.1 // indirect 28 | github.com/google/go-github/v39 v39.2.0 // indirect 29 | github.com/google/go-querystring v1.1.0 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/hashicorp/hcl v1.0.0 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/lib/pq v1.10.9 // indirect 35 | github.com/magiconair/properties v1.8.7 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 40 | github.com/rs/xid v1.6.0 // indirect 41 | github.com/sagikazarmark/locafero v0.6.0 // indirect 42 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 43 | github.com/sourcegraph/conc v0.3.0 // indirect 44 | github.com/spf13/afero v1.11.0 // indirect 45 | github.com/spf13/cast v1.7.0 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | github.com/subosito/gotenv v1.6.0 // indirect 48 | go.uber.org/atomic v1.11.0 // indirect 49 | go.uber.org/multierr v1.11.0 // indirect 50 | golang.org/x/crypto v0.32.0 // indirect 51 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect 52 | golang.org/x/net v0.34.0 // indirect 53 | golang.org/x/oauth2 v0.23.0 // indirect 54 | golang.org/x/sys v0.29.0 // indirect 55 | golang.org/x/text v0.21.0 // indirect 56 | gopkg.in/ini.v1 v1.67.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /migrations/datastore/mysql/README.md: -------------------------------------------------------------------------------- 1 | # Running Warrant with MySQL 2 | 3 | This guide covers how to set up MySQL as a datastore for Warrant. 4 | 5 | Note: Please first refer to the [development guide](/development.md) to ensure that your Go environment is set up and you have checked out the Warrant source or [downloaded a binary](https://github.com/warrant-dev/warrant/releases). 6 | 7 | ## Install MySQL 8 | 9 | Follow the [MySQL Installation Guide](https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/) for your OS to install and start MySQL. For MacOS users, we recommend [installing MySQL using homebrew](https://formulae.brew.sh/formula/mysql). 10 | 11 | ## Warrant configuration 12 | 13 | The Warrant server requires certain configuration, defined either within a `warrant.yaml` file (located within the same directory as the binary) or via environment variables. This configuration includes some common variables and some MySQL specific variables. Here's a sample config: 14 | 15 | ### Sample `warrant.yaml` config 16 | 17 | ```yaml 18 | port: 8000 19 | logLevel: 1 20 | enableAccessLog: true 21 | autoMigrate: true 22 | authentication: 23 | apiKey: replace_with_api_key 24 | datastore: 25 | mysql: 26 | username: replace_with_username 27 | password: replace_with_password 28 | hostname: 127.0.0.1 29 | database: warrant 30 | ``` 31 | 32 | Note: You can create a database via the mysql command line and configure it as the `database` attribute under `datastore`. 33 | 34 | You can also customize your database connection by providing a [DSN (Data Source Name)](https://github.com/go-sql-driver/mysql#dsn-data-source-name). If provided, this string is used to open the given database rather using the individual variables, i.e. `user`, `password`, `hostname`. 35 | 36 | ```yaml 37 | datastore: 38 | mysql: 39 | dsn: root:@tcp(127.0.0.1:3306)/warrant?parseTime=true 40 | ``` 41 | Note: `parseTime=true` must be included when providing a DSN to parse `DATE` and `DATETIME` values to `time.Time`. 42 | 43 | ## Running db migrations 44 | 45 | Warrant uses [golang-migrate](https://github.com/golang-migrate/migrate) to manage sql db migrations. If the `autoMigrate` config flag is set to true, the server will automatically run migrations on start. If you prefer managing migrations and upgrades manually, please set the `autoMigrate` flag to false. 46 | 47 | You can [install golang-migrate yourself](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate) and run the MySQL migrations manually: 48 | 49 | ```shell 50 | migrate -path ./migrations/datastore/mysql/ -database mysql://username:password@hostname/warrant up 51 | ``` 52 | -------------------------------------------------------------------------------- /pkg/object/repository.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/warrant-dev/warrant/pkg/database" 23 | "github.com/warrant-dev/warrant/pkg/service" 24 | ) 25 | 26 | type ObjectRepository interface { 27 | Create(ctx context.Context, object Model) (int64, error) 28 | GetById(ctx context.Context, id int64) (Model, error) 29 | GetByObjectTypeAndId(ctx context.Context, objectType string, objectId string) (Model, error) 30 | BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]Model, error) 31 | List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, *service.Cursor, *service.Cursor, error) 32 | UpdateByObjectTypeAndId(ctx context.Context, objectType string, objectId string, object Model) error 33 | DeleteByObjectTypeAndId(ctx context.Context, objectType string, objectId string) error 34 | DeleteWarrantsMatchingObject(ctx context.Context, objectType string, objectId string) error 35 | DeleteWarrantsMatchingSubject(ctx context.Context, subjectType string, subjectId string) error 36 | } 37 | 38 | func NewRepository(db database.Database) (ObjectRepository, error) { 39 | switch db.Type() { 40 | case database.TypeMySQL: 41 | mysql, ok := db.(*database.MySQL) 42 | if !ok { 43 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeMySQL)) 44 | } 45 | 46 | return NewMySQLRepository(mysql), nil 47 | case database.TypePostgres: 48 | postgres, ok := db.(*database.Postgres) 49 | if !ok { 50 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypePostgres)) 51 | } 52 | 53 | return NewPostgresRepository(postgres), nil 54 | case database.TypeSQLite: 55 | sqlite, ok := db.(*database.SQLite) 56 | if !ok { 57 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeSQLite)) 58 | } 59 | 60 | return NewSQLiteRepository(sqlite), nil 61 | default: 62 | return nil, errors.New(fmt.Sprintf("unsupported database type %s specified", db.Type())) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/authz/warrant/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "time" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | type FilterParams struct { 26 | ObjectType string `json:"objectType,omitempty"` 27 | ObjectId string `json:"objectId,omitempty"` 28 | Relation string `json:"relation,omitempty"` 29 | SubjectType string `json:"subjectType,omitempty"` 30 | SubjectId string `json:"subjectId,omitempty"` 31 | SubjectRelation string `json:"subjectRelation,omitempty"` 32 | } 33 | 34 | func (fp FilterParams) String() string { 35 | s := "" 36 | if len(fp.ObjectType) > 0 { 37 | s = fmt.Sprintf("%s&objectType=%s", s, fp.ObjectType) 38 | } 39 | 40 | if len(fp.ObjectId) > 0 { 41 | s = fmt.Sprintf("%s&objectId=%s", s, fp.ObjectId) 42 | } 43 | 44 | if len(fp.Relation) > 0 { 45 | s = fmt.Sprintf("%s&relation=%s", s, fp.Relation) 46 | } 47 | 48 | if len(fp.SubjectType) > 0 { 49 | s = fmt.Sprintf("%s&subjectType=%s", s, fp.SubjectType) 50 | } 51 | 52 | if len(fp.SubjectId) > 0 { 53 | s = fmt.Sprintf("%s&subjectId=%s", s, fp.SubjectId) 54 | } 55 | 56 | if len(fp.SubjectRelation) > 0 { 57 | s = fmt.Sprintf("%s&subjectRelation=%s", s, fp.SubjectRelation) 58 | } 59 | 60 | return strings.TrimPrefix(s, "&") 61 | } 62 | 63 | const PrimarySortKey = "id" 64 | 65 | type WarrantListParamParser struct{} 66 | 67 | func (parser WarrantListParamParser) GetDefaultSortBy() string { 68 | //nolint:goconst 69 | return "createdAt" 70 | } 71 | 72 | func (parser WarrantListParamParser) GetSupportedSortBys() []string { 73 | return []string{"createdAt"} 74 | } 75 | 76 | func (parser WarrantListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 77 | switch sortBy { 78 | case "createdAt": 79 | value, err := time.Parse(time.RFC3339, val) 80 | if err != nil || value.Equal(time.Time{}) { 81 | return nil, fmt.Errorf("must be a valid time in the format %s", time.RFC3339) 82 | } 83 | 84 | return &value, nil 85 | default: 86 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/authz/warrant/repository.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/pkg/errors" 23 | "github.com/warrant-dev/warrant/pkg/database" 24 | "github.com/warrant-dev/warrant/pkg/service" 25 | ) 26 | 27 | type WarrantRepository interface { 28 | Create(ctx context.Context, warrant Model) (int64, error) 29 | Get(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) (Model, error) 30 | GetByID(ctx context.Context, id int64) (Model, error) 31 | List(ctx context.Context, filterParams FilterParams, listParams service.ListParams) ([]Model, *service.Cursor, *service.Cursor, error) 32 | Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error 33 | } 34 | 35 | func NewRepository(db database.Database) (WarrantRepository, error) { 36 | switch db.Type() { 37 | case database.TypeMySQL: 38 | mysql, ok := db.(*database.MySQL) 39 | if !ok { 40 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeMySQL)) 41 | } 42 | 43 | return NewMySQLRepository(mysql), nil 44 | case database.TypePostgres: 45 | postgres, ok := db.(*database.Postgres) 46 | if !ok { 47 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypePostgres)) 48 | } 49 | 50 | return NewPostgresRepository(postgres), nil 51 | case database.TypeSQLite: 52 | sqlite, ok := db.(*database.SQLite) 53 | if !ok { 54 | return nil, errors.New(fmt.Sprintf("invalid %s database config", database.TypeSQLite)) 55 | } 56 | 57 | return NewSQLiteRepository(sqlite), nil 58 | default: 59 | return nil, errors.New(fmt.Sprintf("unsupported database type %s specified", db.Type())) 60 | } 61 | } 62 | 63 | func BuildQuestionMarkString(numReplacements int) string { 64 | var replacements []string 65 | for i := 0; i < numReplacements; i++ { 66 | replacements = append(replacements, "?") 67 | } 68 | 69 | return strings.Join(replacements, ",") 70 | } 71 | -------------------------------------------------------------------------------- /pkg/authz/objecttype/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "encoding/json" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type Model interface { 25 | GetID() int64 26 | GetTypeId() string 27 | GetDefinition() string 28 | SetDefinition(string) 29 | GetCreatedAt() time.Time 30 | GetUpdatedAt() time.Time 31 | GetDeletedAt() *time.Time 32 | ToObjectTypeSpec() (*ObjectTypeSpec, error) 33 | } 34 | 35 | type ObjectType struct { 36 | ID int64 `mysql:"id" postgres:"id" sqlite:"id"` 37 | TypeId string `mysql:"typeId" postgres:"type_id" sqlite:"typeId"` 38 | Definition string `mysql:"definition" postgres:"definition" sqlite:"definition"` 39 | CreatedAt time.Time `mysql:"createdAt" postgres:"created_at" sqlite:"createdAt"` 40 | UpdatedAt time.Time `mysql:"updatedAt" postgres:"updated_at" sqlite:"updatedAt"` 41 | DeletedAt *time.Time `mysql:"deletedAt" postgres:"deleted_at" sqlite:"deletedAt"` 42 | } 43 | 44 | func (objectType ObjectType) GetID() int64 { 45 | return objectType.ID 46 | } 47 | 48 | func (objectType ObjectType) GetTypeId() string { 49 | return objectType.TypeId 50 | } 51 | 52 | func (objectType ObjectType) GetDefinition() string { 53 | return objectType.Definition 54 | } 55 | 56 | func (objectType *ObjectType) SetDefinition(newDefinition string) { 57 | objectType.Definition = newDefinition 58 | } 59 | 60 | func (objectType ObjectType) GetCreatedAt() time.Time { 61 | return objectType.CreatedAt 62 | } 63 | 64 | func (objectType ObjectType) GetUpdatedAt() time.Time { 65 | return objectType.UpdatedAt 66 | } 67 | 68 | func (objectType ObjectType) GetDeletedAt() *time.Time { 69 | return objectType.DeletedAt 70 | } 71 | 72 | func (objectType ObjectType) ToObjectTypeSpec() (*ObjectTypeSpec, error) { 73 | var objectTypeSpec ObjectTypeSpec 74 | err := json.Unmarshal([]byte(objectType.Definition), &objectTypeSpec) 75 | if err != nil { 76 | return nil, errors.Wrapf(err, "error unmarshaling object type %s", objectType.TypeId) 77 | } 78 | objectTypeSpec.CreatedAt = objectType.CreatedAt 79 | return &objectTypeSpec, nil 80 | } 81 | -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | # Local development 2 | 3 | ## Set up Go 4 | 5 | Warrant is written in Go. Prior to cloning the repo and making any code changes, please ensure that your local Go environment is set up. Refer to the appropriate instructions for your platform [here](https://go.dev/). 6 | 7 | ## Fork & clone repository 8 | 9 | We follow GitHub's fork & pull request model. If you're looking to make code changes, it's easier to do so on your own fork and then contribute pull requests back to the Warrant repo. You can create your own fork of the repo [here](https://github.com/warrant-dev/warrant/fork). 10 | 11 | If you'd just like to checkout the source to build & run, you can clone the repo directly: 12 | 13 | ```shell 14 | git clone git@github.com:warrant-dev/warrant.git 15 | ``` 16 | 17 | Note: It's recommended you clone the repository into a directory relative to your `GOPATH` (e.g. `$GOPATH/src/github.com/warrant-dev`) 18 | 19 | ## Server configuration 20 | To set up your server config file, refer to our [configuration doc](/configuration.md). 21 | 22 | ## Build binary & start server 23 | 24 | After the datastore and configuration are set, build & start the server: 25 | 26 | ```shell 27 | cd cmd/warrant 28 | make dev 29 | ./bin/warrant 30 | ``` 31 | 32 | ## Make requests 33 | 34 | Once the server is running, you can make API requests using curl, any of the [Warrant SDKs](/README.md#sdks), or your favorite API client: 35 | 36 | ```shell 37 | curl -g "http://localhost:port/v1/object-types" -H "Authorization: ApiKey YOUR_KEY" 38 | ``` 39 | 40 | # Running tests 41 | 42 | ## Unit tests 43 | 44 | ```shell 45 | go test -v ./... 46 | ``` 47 | 48 | ## End-to-end API tests 49 | 50 | The Warrant repo contains a suite of e2e tests that test various combinations of API requests. These tests are defined in json files within the `tests/` dir and are executed using [APIRunner](https://github.com/warrant-dev/apirunner). These tests can be run locally: 51 | 52 | ### Install APIRunner 53 | 54 | ```shell 55 | go install github.com/warrant-dev/apirunner/cmd/apirunner@latest 56 | ``` 57 | 58 | ### Define test configuration 59 | 60 | APIRunner tests run based on a simple config file that you need to create in the `tests/` directory: 61 | 62 | ```shell 63 | touch tests/apirunner.conf 64 | ``` 65 | 66 | Add the following to your `tests/apirunner.conf` (replace with your server url and api key): 67 | 68 | ```json 69 | { 70 | "baseUrl": "YOUR_SERVER_URL", 71 | "headers": { 72 | "Authorization" : "ApiKey YOUR_API_KEY" 73 | } 74 | } 75 | ``` 76 | 77 | ### Run tests 78 | 79 | First, make sure your server is running: 80 | 81 | ```shell 82 | ./bin/warrant 83 | ``` 84 | 85 | In a separate shell, run the tests: 86 | 87 | ```shell 88 | cd tests/ 89 | apirunner . '.*' 90 | ``` 91 | -------------------------------------------------------------------------------- /.github/workflows/mysql.yaml: -------------------------------------------------------------------------------- 1 | name: MySQL 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup Go env 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "^1.23.0" 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 2 24 | - name: Build binary 25 | run: make build 26 | working-directory: cmd/warrant 27 | - name: Start & configure mysql 28 | run: | 29 | sudo systemctl start mysql 30 | mysql -e 'CREATE DATABASE warrant;' -uroot -proot 31 | - name: Install apirunner & go-migrate 32 | run: | 33 | go install github.com/warrant-dev/apirunner/cmd/apirunner@latest 34 | go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest 35 | - name: Run datastore migrations 'up' 36 | run: migrate -path ./migrations/datastore/mysql/ -database mysql://root:root@/warrant up 37 | - name: Start test server 38 | id: start-server 39 | run: ./cmd/warrant/bin/warrant > server.log 2>&1 & 40 | env: 41 | WARRANT_AUTOMIGRATE: false 42 | WARRANT_PORT: 8000 43 | WARRANT_LOGLEVEL: 0 44 | WARRANT_ENABLEACCESSLOG: true 45 | WARRANT_AUTHENTICATION_APIKEY: warrant_api_key 46 | WARRANT_CHECK_CONCURRENCY: 4 47 | WARRANT_CHECK_MAXCONCURRENCY: 1000 48 | WARRANT_CHECK_TIMEOUT: 1m 49 | WARRANT_DATASTORE: mysql 50 | WARRANT_DATASTORE_MYSQL_DSN: root:root@tcp(127.0.0.1:3306)/warrant?parseTime=true 51 | WARRANT_DATASTORE_MYSQL_MAXIDLECONNECTIONS: 5 52 | WARRANT_DATASTORE_MYSQL_MAXOPENCONNECTIONS: 5 53 | WARRANT_DATASTORE_MYSQL_CONNMAXIDLETIME: 4h 54 | WARRANT_DATASTORE_MYSQL_CONNMAXLIFETIME: 6h 55 | WARRANT_DATASTORE_MYSQL_READERDSN: root:root@tcp(127.0.0.1:3306)/warrant?parseTime=true 56 | WARRANT_DATASTORE_MYSQL_READERMAXIDLECONNECTIONS: 5 57 | WARRANT_DATASTORE_MYSQL_READERMAXOPENCONNECTIONS: 5 58 | - name: Run apirunner tests 59 | run: | 60 | sleep 3 61 | apirunner tests/ '.*' tests/ci-apirunner.conf 62 | - name: Shutdown test server 63 | if: success() || (failure() && steps.start-server.outcome == 'success') 64 | run: kill -9 `lsof -i:8000 -t` 65 | - name: Run datastore migrations 'down' 66 | run: echo 'y' | migrate -path ./migrations/datastore/mysql/ -database mysql://root:root@/warrant down 67 | - name: Archive server log 68 | if: failure() 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: server-log 72 | path: server.log 73 | if-no-files-found: warn 74 | retention-days: 5 75 | -------------------------------------------------------------------------------- /pkg/authz/check/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | 20 | warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" 21 | ) 22 | 23 | const Authorized = "Authorized" 24 | const NotAuthorized = "Not Authorized" 25 | 26 | type CheckWarrantSpec struct { 27 | ObjectType string `json:"objectType" validate:"required,valid_object_type"` 28 | ObjectId string `json:"objectId" validate:"required,valid_object_id"` 29 | Relation string `json:"relation" validate:"required,valid_relation"` 30 | Subject *warrant.SubjectSpec `json:"subject" validate:"required"` 31 | Context warrant.PolicyContext `json:"context"` 32 | } 33 | 34 | func (spec CheckWarrantSpec) String() string { 35 | return fmt.Sprintf( 36 | "%s:%s#%s@%s%s", 37 | spec.ObjectType, 38 | spec.ObjectId, 39 | spec.Relation, 40 | spec.Subject, 41 | spec.Context, 42 | ) 43 | } 44 | 45 | type CheckSessionWarrantSpec struct { 46 | ObjectType string `json:"objectType" validate:"required,valid_object_type"` 47 | ObjectId string `json:"objectId" validate:"required,valid_object_id"` 48 | Relation string `json:"relation" validate:"required,valid_relation"` 49 | Context warrant.PolicyContext `json:"context"` 50 | } 51 | 52 | type CheckSpec struct { 53 | CheckWarrantSpec 54 | Debug bool `json:"debug" validate:"boolean"` 55 | } 56 | 57 | type CheckManySpec struct { 58 | Op string `json:"op"` 59 | Warrants []CheckWarrantSpec `json:"warrants" validate:"min=1,dive"` 60 | Context warrant.PolicyContext `json:"context"` 61 | Debug bool `json:"debug"` 62 | } 63 | 64 | type SessionCheckManySpec struct { 65 | Op string `json:"op"` 66 | Warrants []CheckSessionWarrantSpec `json:"warrants" validate:"min=1,dive"` 67 | Context warrant.PolicyContext `json:"context"` 68 | Debug bool `json:"debug"` 69 | } 70 | 71 | type CheckResultSpec struct { 72 | Code int64 `json:"code,omitempty"` 73 | Result string `json:"result"` 74 | IsImplicit bool `json:"isImplicit"` 75 | ProcessingTime int64 `json:"processingTime,omitempty"` 76 | DecisionPath map[string][]warrant.WarrantSpec `json:"decisionPath,omitempty"` 77 | } 78 | -------------------------------------------------------------------------------- /pkg/object/role/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type RoleSpec struct { 26 | RoleId string `json:"roleId" validate:"required,valid_object_id"` 27 | Name *string `json:"name"` 28 | Description *string `json:"description"` 29 | CreatedAt time.Time `json:"createdAt"` 30 | } 31 | 32 | func NewRoleSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*RoleSpec, error) { 33 | var ( 34 | name *string 35 | description *string 36 | ) 37 | 38 | if objectSpec.Meta != nil { 39 | if _, exists := objectSpec.Meta["name"]; exists { 40 | nameStr, ok := objectSpec.Meta["name"].(string) 41 | if !ok { 42 | return nil, errors.New("role name has invalid type in object meta") 43 | } 44 | name = &nameStr 45 | } 46 | 47 | if _, exists := objectSpec.Meta["description"]; exists { 48 | descriptionStr, ok := objectSpec.Meta["description"].(string) 49 | if !ok { 50 | return nil, errors.New("role description has invalid type in object meta") 51 | } 52 | description = &descriptionStr 53 | } 54 | } 55 | 56 | return &RoleSpec{ 57 | RoleId: objectSpec.ObjectId, 58 | Name: name, 59 | Description: description, 60 | CreatedAt: objectSpec.CreatedAt, 61 | }, nil 62 | } 63 | 64 | func (spec RoleSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 65 | createObjectSpec := object.CreateObjectSpec{ 66 | ObjectType: objecttype.ObjectTypeRole, 67 | ObjectId: spec.RoleId, 68 | } 69 | 70 | meta := make(map[string]interface{}) 71 | if spec.Name != nil { 72 | meta["name"] = spec.Name 73 | } 74 | 75 | if spec.Description != nil { 76 | meta["description"] = spec.Description 77 | } 78 | 79 | if len(meta) > 0 { 80 | createObjectSpec.Meta = meta 81 | } 82 | 83 | return &createObjectSpec, nil 84 | } 85 | 86 | type UpdateRoleSpec struct { 87 | Name *string `json:"name"` 88 | Description *string `json:"description"` 89 | } 90 | 91 | func (updateSpec UpdateRoleSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 92 | meta := make(map[string]interface{}) 93 | 94 | if updateSpec.Name != nil { 95 | meta["name"] = updateSpec.Name 96 | } 97 | 98 | if updateSpec.Description != nil { 99 | meta["description"] = updateSpec.Description 100 | } 101 | 102 | return &object.UpdateObjectSpec{ 103 | Meta: meta, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/object/feature/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/pkg/errors" 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type FeatureSpec struct { 26 | FeatureId string `json:"featureId" validate:"required,valid_object_id"` 27 | Name *string `json:"name"` 28 | Description *string `json:"description"` 29 | CreatedAt time.Time `json:"createdAt"` 30 | } 31 | 32 | func NewFeatureSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*FeatureSpec, error) { 33 | var ( 34 | name *string 35 | description *string 36 | ) 37 | 38 | if objectSpec.Meta != nil { 39 | if _, exists := objectSpec.Meta["name"]; exists { 40 | nameStr, ok := objectSpec.Meta["name"].(string) 41 | if !ok { 42 | return nil, errors.New("feature name has invalid type in object meta") 43 | } 44 | name = &nameStr 45 | } 46 | 47 | if _, exists := objectSpec.Meta["description"]; exists { 48 | descriptionStr, ok := objectSpec.Meta["description"].(string) 49 | if !ok { 50 | return nil, errors.New("feature description has invalid type in object meta") 51 | } 52 | description = &descriptionStr 53 | } 54 | } 55 | 56 | return &FeatureSpec{ 57 | FeatureId: objectSpec.ObjectId, 58 | Name: name, 59 | Description: description, 60 | CreatedAt: objectSpec.CreatedAt, 61 | }, nil 62 | } 63 | 64 | func (spec FeatureSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 65 | createObjectSpec := object.CreateObjectSpec{ 66 | ObjectType: objecttype.ObjectTypeFeature, 67 | ObjectId: spec.FeatureId, 68 | } 69 | 70 | meta := make(map[string]interface{}) 71 | if spec.Name != nil { 72 | meta["name"] = spec.Name 73 | } 74 | 75 | if spec.Description != nil { 76 | meta["description"] = spec.Description 77 | } 78 | 79 | if len(meta) > 0 { 80 | createObjectSpec.Meta = meta 81 | } 82 | 83 | return &createObjectSpec, nil 84 | } 85 | 86 | type UpdateFeatureSpec struct { 87 | Name *string `json:"name"` 88 | Description *string `json:"description"` 89 | } 90 | 91 | func (updateSpec UpdateFeatureSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 92 | meta := make(map[string]interface{}) 93 | 94 | if updateSpec.Name != nil { 95 | meta["name"] = updateSpec.Name 96 | } 97 | 98 | if updateSpec.Description != nil { 99 | meta["description"] = updateSpec.Description 100 | } 101 | 102 | return &object.UpdateObjectSpec{ 103 | Meta: meta, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/object/permission/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type PermissionSpec struct { 26 | PermissionId string `json:"permissionId" validate:"required,valid_object_id"` 27 | Name *string `json:"name"` 28 | Description *string `json:"description"` 29 | CreatedAt time.Time `json:"createdAt"` 30 | } 31 | 32 | func NewPermissionSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*PermissionSpec, error) { 33 | var ( 34 | name *string 35 | description *string 36 | ) 37 | 38 | if objectSpec.Meta != nil { 39 | if _, exists := objectSpec.Meta["name"]; exists { 40 | nameStr, ok := objectSpec.Meta["name"].(string) 41 | if !ok { 42 | return nil, errors.New("permission name has invalid type in object meta") 43 | } 44 | name = &nameStr 45 | } 46 | 47 | if _, exists := objectSpec.Meta["description"]; exists { 48 | descriptionStr, ok := objectSpec.Meta["description"].(string) 49 | if !ok { 50 | return nil, errors.New("permission description has invalid type in object meta") 51 | } 52 | description = &descriptionStr 53 | } 54 | } 55 | 56 | return &PermissionSpec{ 57 | PermissionId: objectSpec.ObjectId, 58 | Name: name, 59 | Description: description, 60 | CreatedAt: objectSpec.CreatedAt, 61 | }, nil 62 | } 63 | 64 | func (spec PermissionSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 65 | createObjectSpec := object.CreateObjectSpec{ 66 | ObjectType: objecttype.ObjectTypePermission, 67 | ObjectId: spec.PermissionId, 68 | } 69 | 70 | meta := make(map[string]interface{}) 71 | if spec.Name != nil { 72 | meta["name"] = spec.Name 73 | } 74 | 75 | if spec.Description != nil { 76 | meta["description"] = spec.Description 77 | } 78 | 79 | if len(meta) > 0 { 80 | createObjectSpec.Meta = meta 81 | } 82 | 83 | return &createObjectSpec, nil 84 | } 85 | 86 | type UpdatePermissionSpec struct { 87 | Name *string `json:"name"` 88 | Description *string `json:"description"` 89 | } 90 | 91 | func (updateSpec UpdatePermissionSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 92 | meta := make(map[string]interface{}) 93 | 94 | if updateSpec.Name != nil { 95 | meta["name"] = updateSpec.Name 96 | } 97 | 98 | if updateSpec.Description != nil { 99 | meta["description"] = updateSpec.Description 100 | } 101 | 102 | return &object.UpdateObjectSpec{ 103 | Meta: meta, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/authz/check/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "net/http" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | func (svc CheckService) Routes() ([]service.Route, error) { 26 | return []service.Route{ 27 | service.WarrantRoute{ 28 | Pattern: "/v2/authorize", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, authorizeHandler), 31 | OverrideAuthMiddlewareFunc: service.ApiKeyAndSessionAuthMiddleware, 32 | }, 33 | service.WarrantRoute{ 34 | Pattern: "/v2/check", 35 | Method: "POST", 36 | Handler: service.NewRouteHandler(svc, authorizeHandler), 37 | OverrideAuthMiddlewareFunc: service.ApiKeyAndSessionAuthMiddleware, 38 | }, 39 | }, nil 40 | } 41 | 42 | func authorizeHandler(svc CheckService, w http.ResponseWriter, r *http.Request) error { 43 | authInfo, err := service.GetAuthInfoFromRequestContext(r.Context()) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if authInfo != nil && authInfo.UserId != "" { 49 | var sessionCheckManySpec SessionCheckManySpec 50 | err := service.ParseJSONBody(r.Context(), r.Body, &sessionCheckManySpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | warrantSpecs := make([]CheckWarrantSpec, 0) 56 | for _, warrantSpec := range sessionCheckManySpec.Warrants { 57 | warrantSpecs = append(warrantSpecs, CheckWarrantSpec{ 58 | ObjectType: warrantSpec.ObjectType, 59 | ObjectId: warrantSpec.ObjectId, 60 | Relation: warrantSpec.Relation, 61 | Subject: &warrant.SubjectSpec{ 62 | ObjectType: objecttype.ObjectTypeUser, 63 | ObjectId: authInfo.UserId, 64 | }, 65 | }) 66 | } 67 | 68 | checkManySpec := CheckManySpec{ 69 | Op: sessionCheckManySpec.Op, 70 | Warrants: warrantSpecs, 71 | Context: sessionCheckManySpec.Context, 72 | Debug: sessionCheckManySpec.Debug, 73 | } 74 | 75 | checkResult, err := svc.CheckMany(r.Context(), authInfo, &checkManySpec) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | service.SendJSONResponse(w, checkResult) 81 | return nil 82 | } 83 | 84 | var checkManySpec CheckManySpec 85 | err = service.ParseJSONBody(r.Context(), r.Body, &checkManySpec) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | checkResult, err := svc.CheckMany(r.Context(), authInfo, &checkManySpec) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | service.SendJSONResponse(w, checkResult) 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment examples 2 | 3 | Sample deployment configurations for running Warrant. 4 | 5 | ## Docker Compose 6 | 7 | ### Using MySQL as the datastore 8 | 9 | This guide will cover how to self-host Warrant with MySQL as the datastore. Note that Warrant only supports versions of MySQL >= 8.0.32. 10 | 11 | The following [Docker Compose](https://docs.docker.com/compose/) manifest will create a MySQL database, setup the database schema required by Warrant, and start Warrant. You can also accomplish this by running Warrant with [Kubernetes](https://kubernetes.io/): 12 | 13 | ```yaml 14 | version: "3.9" 15 | services: 16 | database: 17 | image: mysql:8.0.32 18 | environment: 19 | MYSQL_USER: replace_with_username 20 | MYSQL_PASSWORD: replace_with_password 21 | ports: 22 | - 3306:3306 23 | healthcheck: 24 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 25 | timeout: 5s 26 | retries: 10 27 | 28 | web: 29 | image: warrantdev/warrant 30 | ports: 31 | - 8000:8000 32 | depends_on: 33 | database: 34 | condition: service_healthy 35 | environment: 36 | WARRANT_PORT: 8000 37 | WARRANT_LOGLEVEL: 1 38 | WARRANT_ENABLEACCESSLOG: true 39 | WARRANT_AUTOMIGRATE: true 40 | WARRANT_CHECK_CONCURRENCY: 4 41 | WARRANT_CHECK_MAXCONCURRENCY: 1000 42 | WARRANT_CHECK_TIMEOUT: 1m 43 | WARRANT_AUTHENTICATION_APIKEY: replace_with_api_key 44 | WARRANT_DATASTORE_MYSQL_USERNAME: replace_with_username 45 | WARRANT_DATASTORE_MYSQL_PASSWORD: replace_with_password 46 | WARRANT_DATASTORE_MYSQL_HOSTNAME: database 47 | WARRANT_DATASTORE_MYSQL_DATABASE: warrant 48 | 49 | ``` 50 | 51 | 52 | ### Using PostgreSQL as the datastore 53 | 54 | This guide will cover how to self-host Warrant with PostgreSQL as the datastore. Note that Warrant only supports versions of PostgreSQL >= 14.7. 55 | 56 | The following [Docker Compose](https://docs.docker.com/compose/) manifest will create a PostgreSQL database, setup the database schema required by Warrant, and start Warrant. You can also accomplish this by running Warrant with [Kubernetes](https://kubernetes.io/): 57 | 58 | ```yaml 59 | version: "3.9" 60 | services: 61 | database: 62 | image: postgres:14.7 63 | environment: 64 | POSTGRES_PASSWORD: replace_with_password 65 | ports: 66 | - 5432:5432 67 | healthcheck: 68 | test: ["CMD", "pg_isready", "-d", "warrant"] 69 | timeout: 5s 70 | retries: 10 71 | 72 | web: 73 | image: warrantdev/warrant 74 | ports: 75 | - 8000:8000 76 | depends_on: 77 | database: 78 | condition: service_healthy 79 | environment: 80 | WARRANT_PORT: 8000 81 | WARRANT_LOGLEVEL: 1 82 | WARRANT_ENABLEACCESSLOG: true 83 | WARRANT_AUTOMIGRATE: true 84 | WARRANT_CHECK_CONCURRENCY: 4 85 | WARRANT_CHECK_MAXCONCURRENCY: 1000 86 | WARRANT_CHECK_TIMEOUT: 1m 87 | WARRANT_AUTHENTICATION_APIKEY: replace_with_api_key 88 | WARRANT_DATASTORE_POSTGRES_USERNAME: postgres 89 | WARRANT_DATASTORE_POSTGRES_PASSWORD: replace_with_password 90 | WARRANT_DATASTORE_POSTGRES_HOSTNAME: database 91 | WARRANT_DATASTORE_POSTGRES_DATABASE: warrant 92 | WARRANT_DATASTORE_POSTGRES_SSLMODE: disable 93 | ``` 94 | -------------------------------------------------------------------------------- /tests/v1/roles-crud.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoredFields": [ 3 | "createdAt" 4 | ], 5 | "tests": [ 6 | { 7 | "name": "createRole", 8 | "request": { 9 | "method": "POST", 10 | "url": "/v1/roles", 11 | "body": { 12 | "roleId": "test-admin" 13 | } 14 | }, 15 | "expectedResponse": { 16 | "statusCode": 200, 17 | "body": { 18 | "roleId": "test-admin", 19 | "name": null, 20 | "description": null 21 | } 22 | } 23 | }, 24 | { 25 | "name": "getRoleById", 26 | "request": { 27 | "method": "GET", 28 | "url": "/v1/roles/test-admin" 29 | }, 30 | "expectedResponse": { 31 | "statusCode": 200, 32 | "body": { 33 | "roleId": "test-admin", 34 | "name": null, 35 | "description": null 36 | } 37 | } 38 | }, 39 | { 40 | "name": "updateRolePOST", 41 | "request": { 42 | "method": "POST", 43 | "url": "/v1/roles/test-admin", 44 | "body": { 45 | "name": "Test Admin" 46 | } 47 | }, 48 | "expectedResponse": { 49 | "statusCode": 200, 50 | "body": { 51 | "roleId": "test-admin", 52 | "name": "Test Admin", 53 | "description": null 54 | } 55 | } 56 | }, 57 | { 58 | "name": "updateRolePUT", 59 | "request": { 60 | "method": "PUT", 61 | "url": "/v1/roles/test-admin", 62 | "body": { 63 | "name": "Test Administrator" 64 | } 65 | }, 66 | "expectedResponse": { 67 | "statusCode": 200, 68 | "body": { 69 | "roleId": "test-admin", 70 | "name": "Test Administrator", 71 | "description": null 72 | } 73 | } 74 | }, 75 | { 76 | "name": "getRoles", 77 | "request": { 78 | "method": "GET", 79 | "url": "/v1/roles" 80 | }, 81 | "expectedResponse": { 82 | "statusCode": 200, 83 | "body": [ 84 | { 85 | "roleId": "test-admin", 86 | "name": "Test Administrator", 87 | "description": null 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | "name": "deleteRole", 94 | "request": { 95 | "method": "DELETE", 96 | "url": "/v1/roles/test-admin" 97 | }, 98 | "expectedResponse": { 99 | "statusCode": 200 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /pkg/object/pricingtier/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 22 | object "github.com/warrant-dev/warrant/pkg/object" 23 | ) 24 | 25 | type PricingTierSpec struct { 26 | PricingTierId string `json:"pricingTierId" validate:"required,valid_object_id"` 27 | Name *string `json:"name"` 28 | Description *string `json:"description"` 29 | CreatedAt time.Time `json:"createdAt"` 30 | } 31 | 32 | func NewPricingTierSpecFromObjectSpec(objectSpec *object.ObjectSpec) (*PricingTierSpec, error) { 33 | var ( 34 | name *string 35 | description *string 36 | ) 37 | 38 | if objectSpec.Meta != nil { 39 | if _, exists := objectSpec.Meta["name"]; exists { 40 | nameStr, ok := objectSpec.Meta["name"].(string) 41 | if !ok { 42 | return nil, errors.New("pricing-tier name has invalid type in object meta") 43 | } 44 | name = &nameStr 45 | } 46 | 47 | if _, exists := objectSpec.Meta["description"]; exists { 48 | descriptionStr, ok := objectSpec.Meta["description"].(string) 49 | if !ok { 50 | return nil, errors.New("pricing-tier description has invalid type in object meta") 51 | } 52 | description = &descriptionStr 53 | } 54 | } 55 | 56 | return &PricingTierSpec{ 57 | PricingTierId: objectSpec.ObjectId, 58 | Name: name, 59 | Description: description, 60 | CreatedAt: objectSpec.CreatedAt, 61 | }, nil 62 | } 63 | 64 | func (spec PricingTierSpec) ToCreateObjectSpec() (*object.CreateObjectSpec, error) { 65 | createObjectSpec := object.CreateObjectSpec{ 66 | ObjectType: objecttype.ObjectTypePricingTier, 67 | ObjectId: spec.PricingTierId, 68 | } 69 | 70 | meta := make(map[string]interface{}) 71 | if spec.Name != nil { 72 | meta["name"] = spec.Name 73 | } 74 | 75 | if spec.Description != nil { 76 | meta["description"] = spec.Description 77 | } 78 | 79 | if len(meta) > 0 { 80 | createObjectSpec.Meta = meta 81 | } 82 | 83 | return &createObjectSpec, nil 84 | } 85 | 86 | type UpdatePricingTierSpec struct { 87 | Name *string `json:"name"` 88 | Description *string `json:"description"` 89 | } 90 | 91 | func (updateSpec UpdatePricingTierSpec) ToUpdateObjectSpec() *object.UpdateObjectSpec { 92 | meta := make(map[string]interface{}) 93 | 94 | if updateSpec.Name != nil { 95 | meta["name"] = updateSpec.Name 96 | } 97 | 98 | if updateSpec.Description != nil { 99 | meta["description"] = updateSpec.Description 100 | } 101 | 102 | return &object.UpdateObjectSpec{ 103 | Meta: meta, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/v1/features-crud.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoredFields": [ 3 | "createdAt" 4 | ], 5 | "tests": [ 6 | { 7 | "name": "createFeature", 8 | "request": { 9 | "method": "POST", 10 | "url": "/v1/features", 11 | "body": { 12 | "featureId": "feature-a" 13 | } 14 | }, 15 | "expectedResponse": { 16 | "statusCode": 200, 17 | "body": { 18 | "featureId": "feature-a", 19 | "name": null, 20 | "description": null 21 | } 22 | } 23 | }, 24 | { 25 | "name": "getFeatureById", 26 | "request": { 27 | "method": "GET", 28 | "url": "/v1/features/feature-a" 29 | }, 30 | "expectedResponse": { 31 | "statusCode": 200, 32 | "body": { 33 | "featureId": "feature-a", 34 | "name": null, 35 | "description": null 36 | } 37 | } 38 | }, 39 | { 40 | "name": "updateFeaturePOST", 41 | "request": { 42 | "method": "POST", 43 | "url": "/v1/features/feature-a", 44 | "body": { 45 | "name": "My Feature" 46 | } 47 | }, 48 | "expectedResponse": { 49 | "statusCode": 200, 50 | "body": { 51 | "featureId": "feature-a", 52 | "name": "My Feature", 53 | "description": null 54 | } 55 | } 56 | }, 57 | { 58 | "name": "updateFeaturePUT", 59 | "request": { 60 | "method": "PUT", 61 | "url": "/v1/features/feature-a", 62 | "body": { 63 | "name": "Feature A" 64 | } 65 | }, 66 | "expectedResponse": { 67 | "statusCode": 200, 68 | "body": { 69 | "featureId": "feature-a", 70 | "name": "Feature A", 71 | "description": null 72 | } 73 | } 74 | }, 75 | { 76 | "name": "getFeatures", 77 | "request": { 78 | "method": "GET", 79 | "url": "/v1/features" 80 | }, 81 | "expectedResponse": { 82 | "statusCode": 200, 83 | "body": [ 84 | { 85 | "featureId": "feature-a", 86 | "name": "Feature A", 87 | "description": null 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | "name": "deleteFeature", 94 | "request": { 95 | "method": "DELETE", 96 | "url": "/v1/features/feature-a" 97 | }, 98 | "expectedResponse": { 99 | "statusCode": 200 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /tests/v1/pricing-tiers-crud.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoredFields": [ 3 | "createdAt" 4 | ], 5 | "tests": [ 6 | { 7 | "name": "createPricingTier", 8 | "request": { 9 | "method": "POST", 10 | "url": "/v1/pricing-tiers", 11 | "body": { 12 | "pricingTierId": "pro" 13 | } 14 | }, 15 | "expectedResponse": { 16 | "statusCode": 200, 17 | "body": { 18 | "pricingTierId": "pro", 19 | "name": null, 20 | "description": null 21 | } 22 | } 23 | }, 24 | { 25 | "name": "getPricingTierById", 26 | "request": { 27 | "method": "GET", 28 | "url": "/v1/pricing-tiers/pro" 29 | }, 30 | "expectedResponse": { 31 | "statusCode": 200, 32 | "body": { 33 | "pricingTierId": "pro", 34 | "name": null, 35 | "description": null 36 | } 37 | } 38 | }, 39 | { 40 | "name": "updatePricingTierPOST", 41 | "request": { 42 | "method": "POST", 43 | "url": "/v1/pricing-tiers/pro", 44 | "body": { 45 | "name": "Pro" 46 | } 47 | }, 48 | "expectedResponse": { 49 | "statusCode": 200, 50 | "body": { 51 | "pricingTierId": "pro", 52 | "name": "Pro", 53 | "description": null 54 | } 55 | } 56 | }, 57 | { 58 | "name": "updatePricingTierPUT", 59 | "request": { 60 | "method": "PUT", 61 | "url": "/v1/pricing-tiers/pro", 62 | "body": { 63 | "name": "Pro Tier" 64 | } 65 | }, 66 | "expectedResponse": { 67 | "statusCode": 200, 68 | "body": { 69 | "pricingTierId": "pro", 70 | "name": "Pro Tier", 71 | "description": null 72 | } 73 | } 74 | }, 75 | { 76 | "name": "getPricingTiers", 77 | "request": { 78 | "method": "GET", 79 | "url": "/v1/pricing-tiers" 80 | }, 81 | "expectedResponse": { 82 | "statusCode": 200, 83 | "body": [ 84 | { 85 | "pricingTierId": "pro", 86 | "name": "Pro Tier", 87 | "description": null 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | "name": "deletePricingTier", 94 | "request": { 95 | "method": "DELETE", 96 | "url": "/v1/pricing-tiers/pro" 97 | }, 98 | "expectedResponse": { 99 | "statusCode": 200 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /pkg/authz/warrant/subject_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/google/go-cmp/cmp" 22 | ) 23 | 24 | func TestToStringDirectSubjectSpec(t *testing.T) { 25 | subject := SubjectSpec{ 26 | ObjectType: "user", 27 | ObjectId: "user-A", 28 | } 29 | expectedSubjectStr := "user:user-A" 30 | actualSubjectStr := subject.String() 31 | if actualSubjectStr != expectedSubjectStr { 32 | t.Fatalf("Expected subject string to be %s, but it was %s", expectedSubjectStr, actualSubjectStr) 33 | } 34 | } 35 | 36 | func TestToStringGroupSubjectSpec(t *testing.T) { 37 | subject := SubjectSpec{ 38 | ObjectType: "role", 39 | ObjectId: "admin", 40 | Relation: "member", 41 | } 42 | expectedSubjectStr := "role:admin#member" 43 | actualSubjectStr := subject.String() 44 | if actualSubjectStr != expectedSubjectStr { 45 | t.Fatalf("Expected subject string to be %s, but it was %s", expectedSubjectStr, actualSubjectStr) 46 | } 47 | } 48 | 49 | func TestStringToSubjectSpecDirectSubjectSpec(t *testing.T) { 50 | subjectStr := "user:user-A" 51 | expectedSubjectSpec := &SubjectSpec{ 52 | ObjectType: "user", 53 | ObjectId: "user-A", 54 | } 55 | actualSubjectSpec, err := StringToSubjectSpec(subjectStr) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if !cmp.Equal(actualSubjectSpec, expectedSubjectSpec) { 61 | t.Fatalf("Expected subject spec to be %v, but it was %v", expectedSubjectSpec, actualSubjectSpec) 62 | } 63 | } 64 | 65 | func TestStringToSubjectSpecGroupSubjectSpec(t *testing.T) { 66 | subjectStr := "role:admin#member" 67 | expectedSubjectSpec := &SubjectSpec{ 68 | ObjectType: "role", 69 | ObjectId: "admin", 70 | Relation: "member", 71 | } 72 | actualSubjectSpec, err := StringToSubjectSpec(subjectStr) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | if !cmp.Equal(actualSubjectSpec, expectedSubjectSpec) { 78 | t.Fatalf("Expected subject spec to be %v, but it was %v", expectedSubjectSpec, actualSubjectSpec) 79 | } 80 | } 81 | 82 | func TestStringToSubjectSpecMultiplePounds(t *testing.T) { 83 | subjectStr := "role:admin#member#" 84 | expectedErrStr := fmt.Sprintf("invalid subject string %s", subjectStr) 85 | _, err := StringToSubjectSpec(subjectStr) 86 | if err == nil || err.Error() != expectedErrStr { 87 | t.Fatalf("Expected err to be %s, but it was %v", expectedErrStr, err) 88 | } 89 | } 90 | 91 | func TestStringToSubjectSpecNoColon(t *testing.T) { 92 | subjectStr := "roleadmin#member" 93 | expectedErrStr := fmt.Sprintf("invalid subject string %s", subjectStr) 94 | _, err := StringToSubjectSpec(subjectStr) 95 | if err == nil || err.Error() != expectedErrStr { 96 | t.Fatalf("Expected err to be %s, but it was %v", expectedErrStr, err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/authz/query/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | const PrimarySortKey = "id" 25 | 26 | type QueryListParamParser struct{} 27 | 28 | func (parser QueryListParamParser) GetDefaultSortBy() string { 29 | return "id" 30 | } 31 | 32 | func (parser QueryListParamParser) GetSupportedSortBys() []string { 33 | return []string{"id", "createdAt"} 34 | } 35 | 36 | func (parser QueryListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { 37 | switch sortBy { 38 | //nolint:goconst 39 | case "createdAt": 40 | value, err := time.Parse(time.RFC3339, val) 41 | if err != nil || value.Equal(time.Time{}) { 42 | return nil, errors.New(fmt.Sprintf("must be a valid time in the format %s", time.RFC3339)) 43 | } 44 | 45 | return &value, nil 46 | default: 47 | return nil, errors.New(fmt.Sprintf("must match type of selected sortBy attribute %s", sortBy)) 48 | } 49 | } 50 | 51 | type ByObjectTypeAndObjectIdAndRelationAsc []QueryResult 52 | 53 | func (res ByObjectTypeAndObjectIdAndRelationAsc) Len() int { return len(res) } 54 | func (res ByObjectTypeAndObjectIdAndRelationAsc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } 55 | func (res ByObjectTypeAndObjectIdAndRelationAsc) Less(i, j int) bool { 56 | if res[i].ObjectType == res[j].ObjectType { 57 | if res[i].ObjectId == res[j].ObjectId { 58 | return res[i].Relation < res[j].Relation 59 | } 60 | return res[i].ObjectId < res[j].ObjectId 61 | } 62 | return res[i].ObjectType < res[j].ObjectType 63 | } 64 | 65 | type ByObjectTypeAndObjectIdAndRelationDesc []QueryResult 66 | 67 | func (res ByObjectTypeAndObjectIdAndRelationDesc) Len() int { return len(res) } 68 | func (res ByObjectTypeAndObjectIdAndRelationDesc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } 69 | func (res ByObjectTypeAndObjectIdAndRelationDesc) Less(i, j int) bool { 70 | if res[i].ObjectType == res[j].ObjectType { 71 | if res[i].ObjectId == res[j].ObjectId { 72 | return res[i].Relation > res[j].Relation 73 | } 74 | return res[i].ObjectId > res[j].ObjectId 75 | } 76 | return res[i].ObjectType > res[j].ObjectType 77 | } 78 | 79 | type ByCreatedAtAsc []QueryResult 80 | 81 | func (res ByCreatedAtAsc) Len() int { return len(res) } 82 | func (res ByCreatedAtAsc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } 83 | func (res ByCreatedAtAsc) Less(i, j int) bool { 84 | return res[i].Warrant.CreatedAt.Before(res[j].Warrant.CreatedAt) 85 | } 86 | 87 | type ByCreatedAtDesc []QueryResult 88 | 89 | func (res ByCreatedAtDesc) Len() int { return len(res) } 90 | func (res ByCreatedAtDesc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } 91 | func (res ByCreatedAtDesc) Less(i, j int) bool { 92 | return res[i].Warrant.CreatedAt.After(res[j].Warrant.CreatedAt) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/object/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "encoding/json" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type Model interface { 26 | GetID() int64 27 | GetObjectType() string 28 | GetObjectId() string 29 | GetMeta() *string 30 | SetMeta(meta map[string]interface{}) error 31 | GetCreatedAt() time.Time 32 | GetUpdatedAt() time.Time 33 | GetDeletedAt() *time.Time 34 | ToObjectSpec() (*ObjectSpec, error) 35 | } 36 | 37 | type Object struct { 38 | ID int64 `mysql:"id" postgres:"id" sqlite:"id"` 39 | ObjectType string `mysql:"objectType" postgres:"object_type" sqlite:"objectType"` 40 | ObjectId string `mysql:"objectId" postgres:"object_id" sqlite:"objectId"` 41 | Meta *string `mysql:"meta" postgres:"meta" sqlite:"meta"` 42 | CreatedAt time.Time `mysql:"createdAt" postgres:"created_at" sqlite:"createdAt"` 43 | UpdatedAt time.Time `mysql:"updatedAt" postgres:"updated_at" sqlite:"updatedAt"` 44 | DeletedAt *time.Time `mysql:"deletedAt" postgres:"deleted_at" sqlite:"deletedAt"` 45 | } 46 | 47 | func (object Object) GetID() int64 { 48 | return object.ID 49 | } 50 | 51 | func (object Object) GetObjectType() string { 52 | return object.ObjectType 53 | } 54 | 55 | func (object Object) GetObjectId() string { 56 | return object.ObjectId 57 | } 58 | 59 | func (object Object) GetMeta() *string { 60 | return object.Meta 61 | } 62 | 63 | func (object *Object) SetMeta(newMeta map[string]interface{}) error { 64 | if len(newMeta) == 0 { 65 | object.Meta = nil 66 | return nil 67 | } 68 | 69 | m, err := json.Marshal(newMeta) 70 | if err != nil { 71 | return service.NewInvalidParameterError("meta", "invalid format") 72 | } 73 | 74 | meta := string(m) 75 | object.Meta = &meta 76 | return nil 77 | } 78 | 79 | func (object Object) GetCreatedAt() time.Time { 80 | return object.CreatedAt 81 | } 82 | 83 | func (object Object) GetUpdatedAt() time.Time { 84 | return object.UpdatedAt 85 | } 86 | 87 | func (object Object) GetDeletedAt() *time.Time { 88 | return object.DeletedAt 89 | } 90 | 91 | func (object Object) ToObjectSpec() (*ObjectSpec, error) { 92 | var meta map[string]interface{} 93 | if object.Meta != nil { 94 | err := json.Unmarshal([]byte(*object.Meta), &meta) 95 | if err != nil { 96 | return nil, errors.Wrapf(err, "error unmarshaling metadata for object %s:%s", object.ObjectType, object.ObjectId) 97 | } 98 | } 99 | 100 | return &ObjectSpec{ 101 | ID: object.ID, 102 | ObjectType: object.ObjectType, 103 | ObjectId: object.ObjectId, 104 | Meta: meta, 105 | CreatedAt: object.CreatedAt, 106 | }, nil 107 | } 108 | -------------------------------------------------------------------------------- /tests/v1/permissions-crud.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoredFields": [ 3 | "createdAt" 4 | ], 5 | "tests": [ 6 | { 7 | "name": "createPermission", 8 | "request": { 9 | "method": "POST", 10 | "url": "/v1/permissions", 11 | "body": { 12 | "permissionId": "new-permission-1" 13 | } 14 | }, 15 | "expectedResponse": { 16 | "statusCode": 200, 17 | "body": { 18 | "permissionId": "new-permission-1", 19 | "name": null, 20 | "description": null 21 | } 22 | } 23 | }, 24 | { 25 | "name": "getPermissionById", 26 | "request": { 27 | "method": "GET", 28 | "url": "/v1/permissions/new-permission-1" 29 | }, 30 | "expectedResponse": { 31 | "statusCode": 200, 32 | "body": { 33 | "permissionId": "new-permission-1", 34 | "name": null, 35 | "description": null 36 | } 37 | } 38 | }, 39 | { 40 | "name": "updatePermissionPOST", 41 | "request": { 42 | "method": "POST", 43 | "url": "/v1/permissions/new-permission-1", 44 | "body": { 45 | "name": "New Permission" 46 | } 47 | }, 48 | "expectedResponse": { 49 | "statusCode": 200, 50 | "body": { 51 | "permissionId": "new-permission-1", 52 | "name": "New Permission", 53 | "description": null 54 | } 55 | } 56 | }, 57 | { 58 | "name": "updatePermissionPUT", 59 | "request": { 60 | "method": "PUT", 61 | "url": "/v1/permissions/new-permission-1", 62 | "body": { 63 | "name": "My Permission" 64 | } 65 | }, 66 | "expectedResponse": { 67 | "statusCode": 200, 68 | "body": { 69 | "permissionId": "new-permission-1", 70 | "name": "My Permission", 71 | "description": null 72 | } 73 | } 74 | }, 75 | { 76 | "name": "getPermissions", 77 | "request": { 78 | "method": "GET", 79 | "url": "/v1/permissions" 80 | }, 81 | "expectedResponse": { 82 | "statusCode": 200, 83 | "body": [ 84 | { 85 | "permissionId": "new-permission-1", 86 | "name": "My Permission", 87 | "description": null 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | "name": "deletePermission", 94 | "request": { 95 | "method": "DELETE", 96 | "url": "/v1/permissions/new-permission-1" 97 | }, 98 | "expectedResponse": { 99 | "statusCode": 200 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/postgres.yaml: -------------------------------------------------------------------------------- 1 | name: PostgreSQL 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup Go env 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "^1.23.0" 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 2 24 | - name: Build binary 25 | run: make build 26 | working-directory: cmd/warrant 27 | - name: Start & configure postgres 28 | run: | 29 | sudo systemctl start postgresql.service 30 | pg_isready 31 | sudo -u postgres psql --command="CREATE USER warrant_user PASSWORD 'db_password'" 32 | sudo -u postgres psql --command="ALTER USER warrant_user CREATEDB" --command="\du" 33 | sudo -u postgres createdb --owner=warrant_user warrant_user 34 | sudo -u postgres createdb --owner=warrant_user warrant 35 | sudo -u postgres psql --command="\l" 36 | - name: Install apirunner & go-migrate 37 | run: | 38 | go install github.com/warrant-dev/apirunner/cmd/apirunner@latest 39 | go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest 40 | - name: Run datastore migrations 'up' 41 | run: migrate -path ./migrations/datastore/postgres/ -database postgres://warrant_user:db_password@/warrant?sslmode=disable up 42 | - name: Start test server 43 | id: start-server 44 | run: ./cmd/warrant/bin/warrant > server.log 2>&1 & 45 | env: 46 | WARRANT_AUTOMIGRATE: false 47 | WARRANT_PORT: 8000 48 | WARRANT_LOGLEVEL: 0 49 | WARRANT_ENABLEACCESSLOG: true 50 | WARRANT_AUTHENTICATION_APIKEY: warrant_api_key 51 | WARRANT_CHECK_CONCURRENCY: 4 52 | WARRANT_CHECK_MAXCONCURRENCY: 1000 53 | WARRANT_CHECK_TIMEOUT: 1m 54 | WARRANT_DATASTORE: postgres 55 | WARRANT_DATASTORE_POSTGRES_DSN: postgresql://warrant_user:db_password@localhost:5432/warrant?sslmode=disable 56 | WARRANT_DATASTORE_POSTGRES_MAXIDLECONNECTIONS: 5 57 | WARRANT_DATASTORE_POSTGRES_MAXOPENCONNECTIONS: 5 58 | WARRANT_DATASTORE_POSTGRES_CONNMAXIDLETIME: 4h 59 | WARRANT_DATASTORE_POSTGRES_CONNMAXLIFETIME: 6h 60 | WARRANT_DATASTORE_POSTGRES_READERDSN: postgresql://warrant_user:db_password@localhost:5432/warrant?sslmode=disable 61 | WARRANT_DATASTORE_POSTGRES_READERMAXIDLECONNECTIONS: 5 62 | WARRANT_DATASTORE_POSTGRES_READERMAXOPENCONNECTIONS: 5 63 | - name: Run apirunner tests 64 | run: | 65 | sleep 3 66 | apirunner tests/ '.*' tests/ci-apirunner.conf 67 | - name: Shutdown test server 68 | if: success() || (failure() && steps.start-server.outcome == 'success') 69 | run: kill -9 `lsof -i:8000 -t` 70 | - name: Run datastore migrations 'down' 71 | run: echo 'y' | migrate -path ./migrations/datastore/postgres/ -database postgres://warrant_user:db_password@/warrant?sslmode=disable down 72 | - name: Archive server log 73 | if: failure() 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: server-log 77 | path: server.log 78 | if-no-files-found: warn 79 | retention-days: 5 80 | -------------------------------------------------------------------------------- /pkg/object/role/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type RoleService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *RoleService { 31 | return &RoleService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc RoleService) Create(ctx context.Context, roleSpec RoleSpec) (*RoleSpec, error) { 38 | var createdRoleSpec *RoleSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := roleSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdRoleSpec, err = NewRoleSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdRoleSpec, nil 62 | } 63 | 64 | func (svc RoleService) GetByRoleId(ctx context.Context, roleId string) (*RoleSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypeRole, roleId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewRoleSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc RoleService) List(ctx context.Context, listParams service.ListParams) ([]RoleSpec, error) { 74 | roleSpecs := make([]RoleSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypeRole}, listParams) 76 | if err != nil { 77 | return roleSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | roleSpec, err := NewRoleSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return roleSpecs, err 84 | } 85 | 86 | roleSpecs = append(roleSpecs, *roleSpec) 87 | } 88 | 89 | return roleSpecs, nil 90 | } 91 | 92 | func (svc RoleService) UpdateByRoleId(ctx context.Context, roleId string, roleSpec UpdateRoleSpec) (*RoleSpec, error) { 93 | var updatedRoleSpec *RoleSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypeRole, roleId, *roleSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedRoleSpec, err = NewRoleSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedRoleSpec, nil 112 | } 113 | 114 | func (svc RoleService) DeleteByRoleId(ctx context.Context, roleId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypeRole, roleId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/object/user/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type UserService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *UserService { 31 | return &UserService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc UserService) Create(ctx context.Context, userSpec UserSpec) (*UserSpec, error) { 38 | var createdUserSpec *UserSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := userSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdUserSpec, err = NewUserSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdUserSpec, nil 62 | } 63 | 64 | func (svc UserService) GetByUserId(ctx context.Context, userId string) (*UserSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypeUser, userId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewUserSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc UserService) List(ctx context.Context, listParams service.ListParams) ([]UserSpec, error) { 74 | userSpecs := make([]UserSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypeUser}, listParams) 76 | if err != nil { 77 | return userSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | userSpec, err := NewUserSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return userSpecs, err 84 | } 85 | 86 | userSpecs = append(userSpecs, *userSpec) 87 | } 88 | 89 | return userSpecs, nil 90 | } 91 | 92 | func (svc UserService) UpdateByUserId(ctx context.Context, userId string, userSpec UpdateUserSpec) (*UserSpec, error) { 93 | var updatedUserSpec *UserSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypeUser, userId, *userSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedUserSpec, err = NewUserSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedUserSpec, nil 112 | } 113 | 114 | func (svc UserService) DeleteByUserId(ctx context.Context, userId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypeUser, userId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package stats 16 | 17 | import ( 18 | "context" 19 | "net/http" 20 | "sync" 21 | "time" 22 | 23 | "github.com/rs/zerolog" 24 | ) 25 | 26 | type Stat struct { 27 | Store string 28 | Tag string 29 | Duration time.Duration 30 | } 31 | 32 | func (s Stat) MarshalZerologObject(e *zerolog.Event) { 33 | e.Str("store", s.Store).Str("tag", s.Tag).Dur("duration", s.Duration) 34 | } 35 | 36 | type RequestStats struct { 37 | mutex sync.Mutex 38 | stats []Stat 39 | } 40 | 41 | func (s *RequestStats) RecordStat(stat Stat) { 42 | s.mutex.Lock() 43 | s.stats = append(s.stats, stat) 44 | s.mutex.Unlock() 45 | } 46 | 47 | func (s *RequestStats) NumStats() int { 48 | s.mutex.Lock() 49 | numStats := len(s.stats) 50 | s.mutex.Unlock() 51 | return numStats 52 | } 53 | 54 | func (s *RequestStats) MarshalZerologObject(e *zerolog.Event) { 55 | arr := zerolog.Arr() 56 | s.mutex.Lock() 57 | for _, stat := range s.stats { 58 | arr.Object(stat) 59 | } 60 | s.mutex.Unlock() 61 | e.Array("stats", arr) 62 | } 63 | 64 | type requestStatsKey struct{} 65 | type statTagKey struct{} 66 | 67 | // Create & inject a 'per-request' stats object into request context. 68 | func RequestStatsMiddleware(next http.Handler) http.Handler { 69 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | reqStats := RequestStats{ 71 | stats: make([]Stat, 0), 72 | } 73 | ctxWithReqStats := context.WithValue(r.Context(), requestStatsKey{}, &reqStats) 74 | next.ServeHTTP(w, r.WithContext(ctxWithReqStats)) 75 | }) 76 | } 77 | 78 | // Get RequestStats from ctx, if present. 79 | func GetRequestStatsFromContext(ctx context.Context) *RequestStats { 80 | if reqStats, ok := ctx.Value(requestStatsKey{}).(*RequestStats); ok { 81 | return reqStats 82 | } 83 | return nil 84 | } 85 | 86 | // Returns a blank context with only parent's existing *RequestStats (if present). 87 | func BlankContextWithRequestStats(parent context.Context) context.Context { 88 | stats := GetRequestStatsFromContext(parent) 89 | if stats != nil { 90 | return context.WithValue(context.Background(), requestStatsKey{}, stats) 91 | } 92 | return context.Background() 93 | } 94 | 95 | // Append a new Stat to the RequestStats obj in provided context, if present. 96 | func RecordStat(ctx context.Context, store string, tag string, start time.Time) { 97 | if reqStats, ok := ctx.Value(requestStatsKey{}).(*RequestStats); ok { 98 | if tagPrefix, ctxHasTag := ctx.Value(statTagKey{}).(string); ctxHasTag { 99 | tag = tagPrefix + "." + tag 100 | } 101 | reqStats.RecordStat(Stat{ 102 | Store: store, 103 | Tag: tag, 104 | Duration: time.Since(start), 105 | }) 106 | } 107 | } 108 | 109 | // Returns a new context with given crumb appended to existing tag, if present. Otherwise, tracks the new tag in returned context. Useful for adding breadcrumbs to a Stat prior to a recording it. 110 | func ContextWithTagCrumb(ctx context.Context, crumb string) context.Context { 111 | if tag, ok := ctx.Value(statTagKey{}).(string); ok { 112 | return context.WithValue(ctx, statTagKey{}, tag+"."+crumb) 113 | } 114 | return context.WithValue(ctx, statTagKey{}, crumb) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/authz/warrant/policy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "crypto/sha256" 19 | "encoding/hex" 20 | "fmt" 21 | "sort" 22 | "strings" 23 | "time" 24 | 25 | "github.com/antonmedv/expr" 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | type Policy string 30 | 31 | type PolicyContext map[string]interface{} 32 | 33 | func (pc PolicyContext) String() string { 34 | if len(pc) == 0 { 35 | return "" 36 | } 37 | 38 | contextKeys := make([]string, 0) 39 | for key := range pc { 40 | contextKeys = append(contextKeys, key) 41 | } 42 | sort.Strings(contextKeys) 43 | 44 | keyValuePairs := make([]string, 0) 45 | for _, key := range contextKeys { 46 | keyValuePairs = append(keyValuePairs, fmt.Sprintf("%s=%v", key, pc[key])) 47 | } 48 | 49 | return fmt.Sprintf("[%s]", strings.Join(keyValuePairs, " ")) 50 | } 51 | 52 | func defaultExprOptions(ctx PolicyContext) []expr.Option { 53 | var opts []expr.Option 54 | if ctx != nil { 55 | opts = append(opts, expr.Env(ctx)) 56 | } 57 | 58 | opts = append(opts, expr.Function( 59 | "expiresIn", 60 | func(params ...interface{}) (interface{}, error) { 61 | durationStr := params[0].(string) 62 | duration, err := time.ParseDuration(durationStr) 63 | if err != nil { 64 | return false, fmt.Errorf("invalid duration string %s", durationStr) 65 | } 66 | 67 | warrantCreatedAt := ctx["warrant"].(*Warrant).CreatedAt 68 | return bool(time.Now().Before(warrantCreatedAt.Add(duration))), nil 69 | }, 70 | new(func(string) bool), 71 | )) 72 | opts = append(opts, 73 | expr.AllowUndefinedVariables(), 74 | expr.AsBool(), 75 | ) 76 | 77 | return opts 78 | } 79 | 80 | func (policy Policy) Validate() error { 81 | _, err := expr.Compile(string(policy), defaultExprOptions(nil)...) 82 | if err != nil { 83 | return errors.Wrapf(err, "error validating policy '%s'", policy) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (policy Policy) Eval(ctx PolicyContext) (bool, error) { 90 | program, err := expr.Compile(string(policy), defaultExprOptions(ctx)...) 91 | if err != nil { 92 | return false, errors.Wrapf(err, "error compiling policy '%s'", policy) 93 | } 94 | 95 | match, err := expr.Run(program, ctx) 96 | if err != nil { 97 | return false, errors.Wrapf(err, "error evaluating policy '%s'", policy) 98 | } 99 | 100 | return match.(bool), nil 101 | } 102 | 103 | func (policy Policy) Hash() string { 104 | if policy == "" { 105 | return "" 106 | } 107 | 108 | hash := sha256.Sum256([]byte(policy)) 109 | return hex.EncodeToString(hash[:]) 110 | } 111 | 112 | func (policy Policy) Or(or Policy) Policy { 113 | if policy == "" { 114 | return or 115 | } 116 | if or == "" { 117 | return policy 118 | } 119 | if policy == or { 120 | return policy 121 | } 122 | 123 | return Policy(fmt.Sprintf("(%s) || (%s)", policy, or)) 124 | } 125 | 126 | func (policy Policy) And(and Policy) Policy { 127 | if policy == "" { 128 | return and 129 | } 130 | if and == "" { 131 | return policy 132 | } 133 | if policy == and { 134 | return policy 135 | } 136 | 137 | return Policy(fmt.Sprintf("(%s) && (%s)", policy, and)) 138 | } 139 | 140 | func Not(p Policy) Policy { 141 | if p == "" { 142 | return p 143 | } 144 | return Policy(fmt.Sprintf("!(%s)", p)) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/object/tenant/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tenant 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type TenantService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *TenantService { 31 | return &TenantService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc TenantService) Create(ctx context.Context, tenantSpec TenantSpec) (*TenantSpec, error) { 38 | var createdTenantSpec *TenantSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := tenantSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdTenantSpec, err = NewTenantSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdTenantSpec, nil 62 | } 63 | 64 | func (svc TenantService) GetByTenantId(ctx context.Context, tenantId string) (*TenantSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypeTenant, tenantId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewTenantSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc TenantService) List(ctx context.Context, listParams service.ListParams) ([]TenantSpec, error) { 74 | tenantSpecs := make([]TenantSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypeTenant}, listParams) 76 | if err != nil { 77 | return tenantSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | tenantSpec, err := NewTenantSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return tenantSpecs, err 84 | } 85 | 86 | tenantSpecs = append(tenantSpecs, *tenantSpec) 87 | } 88 | 89 | return tenantSpecs, nil 90 | } 91 | 92 | func (svc TenantService) UpdateByTenantId(ctx context.Context, tenantId string, tenantSpec UpdateTenantSpec) (*TenantSpec, error) { 93 | var updatedTenantSpec *TenantSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypeTenant, tenantId, *tenantSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedTenantSpec, err = NewTenantSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedTenantSpec, nil 112 | } 113 | 114 | func (svc TenantService) DeleteByTenantId(ctx context.Context, tenantId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypeTenant, tenantId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/authz/query/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/pkg/errors" 23 | warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" 24 | "github.com/warrant-dev/warrant/pkg/service" 25 | ) 26 | 27 | type Query struct { 28 | Expand bool 29 | SelectSubjects *SelectSubjects 30 | SelectObjects *SelectObjects 31 | Context warrant.PolicyContext 32 | } 33 | 34 | func (q *Query) WithContext(contextString string) error { 35 | var context warrant.PolicyContext 36 | err := json.Unmarshal([]byte(contextString), &context) 37 | if err != nil { 38 | return errors.Wrap(err, "query: error parsing query context") 39 | } 40 | 41 | q.Context = context 42 | return nil 43 | } 44 | 45 | func (q *Query) String() string { 46 | var str string 47 | if q.Expand { 48 | str = "select" 49 | } else { 50 | str = "select explicit" 51 | } 52 | 53 | if q.SelectObjects != nil { 54 | return fmt.Sprintf("%s %s %s", str, q.SelectObjects.String(), q.Context.String()) 55 | } else if q.SelectSubjects != nil { 56 | return fmt.Sprintf("%s %s %s", str, q.SelectSubjects.String(), q.Context.String()) 57 | } 58 | 59 | return "" 60 | } 61 | 62 | type SelectSubjects struct { 63 | ForObject *Resource 64 | Relations []string 65 | SubjectTypes []string 66 | } 67 | 68 | func (s SelectSubjects) String() string { 69 | str := fmt.Sprintf("%s of type %s", strings.Join(s.Relations, ", "), strings.Join(s.SubjectTypes, ", ")) 70 | if s.ForObject != nil { 71 | str = fmt.Sprintf("%s for %s", str, s.ForObject.String()) 72 | } 73 | 74 | return str 75 | } 76 | 77 | type SelectObjects struct { 78 | ObjectTypes []string 79 | Relations []string 80 | WhereSubject *Resource 81 | } 82 | 83 | func (s SelectObjects) String() string { 84 | str := strings.Join(s.ObjectTypes, ", ") 85 | if s.WhereSubject != nil { 86 | str = fmt.Sprintf("%s where %s", str, s.WhereSubject.String()) 87 | } 88 | 89 | return fmt.Sprintf("%s is %s", str, strings.Join(s.Relations, ", ")) 90 | } 91 | 92 | type Resource struct { 93 | Type string 94 | Id string 95 | } 96 | 97 | func (res Resource) String() string { 98 | return fmt.Sprintf("%s:%s", res.Type, res.Id) 99 | } 100 | 101 | type QueryHaving struct { 102 | ObjectType string `json:"objectType,omitempty"` 103 | ObjectId string `json:"objectId,omitempty"` 104 | Relation string `json:"relation,omitempty"` 105 | SubjectType string `json:"subjectType,omitempty"` 106 | SubjectId string `json:"subjectId,omitempty"` 107 | } 108 | 109 | type QueryResult struct { 110 | ObjectType string `json:"objectType"` 111 | ObjectId string `json:"objectId"` 112 | Relation string `json:"relation"` 113 | Warrant warrant.WarrantSpec `json:"warrant"` 114 | IsImplicit bool `json:"isImplicit"` 115 | Meta map[string]interface{} `json:"meta,omitempty"` 116 | } 117 | 118 | type QueryResponseV1 struct { 119 | Results []QueryResult `json:"results"` 120 | LastId string `json:"lastId,omitempty"` 121 | } 122 | 123 | type QueryResponseV2 struct { 124 | Results []QueryResult `json:"results"` 125 | PrevCursor *service.Cursor `json:"prevCursor,omitempty"` 126 | NextCursor *service.Cursor `json:"nextCursor,omitempty"` 127 | } 128 | -------------------------------------------------------------------------------- /pkg/object/feature/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type FeatureService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *FeatureService { 31 | return &FeatureService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc FeatureService) Create(ctx context.Context, featureSpec FeatureSpec) (*FeatureSpec, error) { 38 | var createdFeatureSpec *FeatureSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := featureSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdFeatureSpec, err = NewFeatureSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdFeatureSpec, nil 62 | } 63 | 64 | func (svc FeatureService) GetByFeatureId(ctx context.Context, featureId string) (*FeatureSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypeFeature, featureId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewFeatureSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc FeatureService) List(ctx context.Context, listParams service.ListParams) ([]FeatureSpec, error) { 74 | featureSpecs := make([]FeatureSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypeFeature}, listParams) 76 | if err != nil { 77 | return featureSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | featureSpec, err := NewFeatureSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return featureSpecs, err 84 | } 85 | 86 | featureSpecs = append(featureSpecs, *featureSpec) 87 | } 88 | 89 | return featureSpecs, nil 90 | } 91 | 92 | func (svc FeatureService) UpdateByFeatureId(ctx context.Context, featureId string, featureSpec UpdateFeatureSpec) (*FeatureSpec, error) { 93 | var updatedFeatureSpec *FeatureSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypeFeature, featureId, *featureSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedFeatureSpec, err = NewFeatureSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedFeatureSpec, nil 112 | } 113 | 114 | func (svc FeatureService) DeleteByFeatureId(ctx context.Context, featureId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypeFeature, featureId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/object/user/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc UserService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/users", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/users", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[UserListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/users/{userId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // delete 51 | service.WarrantRoute{ 52 | Pattern: "/v1/users/{userId}", 53 | Method: "DELETE", 54 | Handler: service.NewRouteHandler(svc, deleteHandler), 55 | }, 56 | 57 | // update 58 | service.WarrantRoute{ 59 | Pattern: "/v1/users/{userId}", 60 | Method: "POST", 61 | Handler: service.NewRouteHandler(svc, updateHandler), 62 | }, 63 | service.WarrantRoute{ 64 | Pattern: "/v1/users/{userId}", 65 | Method: "PUT", 66 | Handler: service.NewRouteHandler(svc, updateHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc UserService, w http.ResponseWriter, r *http.Request) error { 72 | var userSpec UserSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &userSpec) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdUser, err := svc.Create(r.Context(), userSpec) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdUser) 84 | return nil 85 | } 86 | 87 | func getHandler(svc UserService, w http.ResponseWriter, r *http.Request) error { 88 | userId := mux.Vars(r)["userId"] 89 | user, err := svc.GetByUserId(r.Context(), userId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, user) 95 | return nil 96 | } 97 | 98 | func listHandler(svc UserService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[UserListParamParser](r.Context()) 100 | users, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, users) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc UserService, w http.ResponseWriter, r *http.Request) error { 110 | var updateUser UpdateUserSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updateUser) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | userId := mux.Vars(r)["userId"] 117 | updatedUser, err := svc.UpdateByUserId(r.Context(), userId, updateUser) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedUser) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc UserService, w http.ResponseWriter, r *http.Request) error { 127 | userId := mux.Vars(r)["userId"] 128 | err := svc.DeleteByUserId(r.Context(), userId) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | w.Header().Set("Content-type", "application/json") 134 | w.WriteHeader(http.StatusOK) 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /pkg/object/role/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc RoleService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/roles", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/roles", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[RoleListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/roles/{roleId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // update 51 | service.WarrantRoute{ 52 | Pattern: "/v1/roles/{roleId}", 53 | Method: "POST", 54 | Handler: service.NewRouteHandler(svc, updateHandler), 55 | }, 56 | service.WarrantRoute{ 57 | Pattern: "/v1/roles/{roleId}", 58 | Method: "PUT", 59 | Handler: service.NewRouteHandler(svc, updateHandler), 60 | }, 61 | 62 | // delete 63 | service.WarrantRoute{ 64 | Pattern: "/v1/roles/{roleId}", 65 | Method: "DELETE", 66 | Handler: service.NewRouteHandler(svc, deleteHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc RoleService, w http.ResponseWriter, r *http.Request) error { 72 | var newRole RoleSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &newRole) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdRole, err := svc.Create(r.Context(), newRole) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdRole) 84 | return nil 85 | } 86 | 87 | func getHandler(svc RoleService, w http.ResponseWriter, r *http.Request) error { 88 | roleId := mux.Vars(r)["roleId"] 89 | role, err := svc.GetByRoleId(r.Context(), roleId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, role) 95 | return nil 96 | } 97 | 98 | func listHandler(svc RoleService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[RoleListParamParser](r.Context()) 100 | roles, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, roles) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc RoleService, w http.ResponseWriter, r *http.Request) error { 110 | var updateRole UpdateRoleSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updateRole) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | roleId := mux.Vars(r)["roleId"] 117 | updatedRole, err := svc.UpdateByRoleId(r.Context(), roleId, updateRole) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedRole) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc RoleService, w http.ResponseWriter, r *http.Request) error { 127 | roleId := mux.Vars(r)["roleId"] 128 | if roleId == "" { 129 | return service.NewMissingRequiredParameterError("roleId") 130 | } 131 | 132 | err := svc.DeleteByRoleId(r.Context(), roleId) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/authz/query/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/warrant-dev/warrant/pkg/service" 21 | ) 22 | 23 | func (svc QueryService) Routes() ([]service.Route, error) { 24 | return []service.Route{ 25 | service.WarrantRoute{ 26 | Pattern: "/v1/query", 27 | Method: "GET", 28 | Handler: service.ChainMiddleware( 29 | service.NewRouteHandler(svc, queryV1), 30 | service.ListMiddleware[QueryListParamParser], 31 | ), 32 | }, 33 | service.WarrantRoute{ 34 | Pattern: "/v2/query", 35 | Method: "GET", 36 | Handler: service.ChainMiddleware( 37 | service.NewRouteHandler(svc, queryV2), 38 | service.ListMiddleware[QueryListParamParser], 39 | ), 40 | }, 41 | }, nil 42 | } 43 | 44 | func queryV1(svc QueryService, w http.ResponseWriter, r *http.Request) error { 45 | queryParams := r.URL.Query() 46 | queryString := queryParams.Get("q") 47 | query, err := NewQueryFromString(queryString) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if queryParams.Has("context") { 53 | err = query.WithContext(queryParams.Get("context")) 54 | if err != nil { 55 | return service.NewInvalidParameterError("context", "invalid") 56 | } 57 | } 58 | 59 | listParams := service.GetListParamsFromContext[QueryListParamParser](r.Context()) 60 | // create next cursor from lastId or afterId param 61 | if r.URL.Query().Has("lastId") { 62 | lastIdCursor, err := service.NewCursorFromBase64String(r.URL.Query().Get("lastId"), QueryListParamParser{}, listParams.SortBy) 63 | if err != nil { 64 | return service.NewInvalidParameterError("lastId", "invalid lastId") 65 | } 66 | 67 | listParams.WithNextCursor(lastIdCursor) 68 | } else if r.URL.Query().Has("afterId") { 69 | afterIdCursor, err := service.NewCursorFromBase64String(r.URL.Query().Get("afterId"), QueryListParamParser{}, listParams.SortBy) 70 | if err != nil { 71 | return service.NewInvalidParameterError("afterId", "invalid afterId") 72 | } 73 | 74 | listParams.WithNextCursor(afterIdCursor) 75 | } 76 | 77 | results, _, nextCursor, err := svc.Query(r.Context(), query, listParams) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | var newLastId string 83 | if nextCursor != nil { 84 | base64EncodedNextCursor, err := nextCursor.ToBase64String() 85 | if err != nil { 86 | return err 87 | } 88 | newLastId = base64EncodedNextCursor 89 | } 90 | 91 | service.SendJSONResponse(w, QueryResponseV1{ 92 | Results: results, 93 | LastId: newLastId, 94 | }) 95 | return nil 96 | } 97 | 98 | func queryV2(svc QueryService, w http.ResponseWriter, r *http.Request) error { 99 | queryParams := r.URL.Query() 100 | queryString := queryParams.Get("q") 101 | query, err := NewQueryFromString(queryString) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if queryParams.Has("context") { 107 | err = query.WithContext(queryParams.Get("context")) 108 | if err != nil { 109 | return service.NewInvalidParameterError("context", "invalid") 110 | } 111 | } 112 | 113 | listParams := service.GetListParamsFromContext[QueryListParamParser](r.Context()) 114 | results, prevCursor, nextCursor, err := svc.Query(r.Context(), query, listParams) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | service.SendJSONResponse(w, QueryResponseV2{ 120 | Results: results, 121 | PrevCursor: prevCursor, 122 | NextCursor: nextCursor, 123 | }) 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/object/tenant/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tenant 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc TenantService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/tenants", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/tenants", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[TenantListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/tenants/{tenantId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // update 51 | service.WarrantRoute{ 52 | Pattern: "/v1/tenants/{tenantId}", 53 | Method: "POST", 54 | Handler: service.NewRouteHandler(svc, updateHandler), 55 | }, 56 | service.WarrantRoute{ 57 | Pattern: "/v1/tenants/{tenantId}", 58 | Method: "PUT", 59 | Handler: service.NewRouteHandler(svc, updateHandler), 60 | }, 61 | 62 | // delete 63 | service.WarrantRoute{ 64 | Pattern: "/v1/tenants/{tenantId}", 65 | Method: "DELETE", 66 | Handler: service.NewRouteHandler(svc, deleteHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc TenantService, w http.ResponseWriter, r *http.Request) error { 72 | var newTenant TenantSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &newTenant) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdTenant, err := svc.Create(r.Context(), newTenant) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdTenant) 84 | return nil 85 | } 86 | 87 | func getHandler(svc TenantService, w http.ResponseWriter, r *http.Request) error { 88 | tenantId := mux.Vars(r)["tenantId"] 89 | tenant, err := svc.GetByTenantId(r.Context(), tenantId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, tenant) 95 | return nil 96 | } 97 | 98 | func listHandler(svc TenantService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[TenantListParamParser](r.Context()) 100 | tenants, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, tenants) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc TenantService, w http.ResponseWriter, r *http.Request) error { 110 | var updateTenant UpdateTenantSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updateTenant) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | tenantId := mux.Vars(r)["tenantId"] 117 | updatedTenant, err := svc.UpdateByTenantId(r.Context(), tenantId, updateTenant) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedTenant) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc TenantService, w http.ResponseWriter, r *http.Request) error { 127 | tenantId := mux.Vars(r)["tenantId"] 128 | err := svc.DeleteByTenantId(r.Context(), tenantId) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | w.Header().Set("Content-type", "application/json") 134 | w.WriteHeader(http.StatusOK) 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /pkg/object/permission/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type PermissionService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *PermissionService { 31 | return &PermissionService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc PermissionService) Create(ctx context.Context, permissionSpec PermissionSpec) (*PermissionSpec, error) { 38 | var createdPermissionSpec *PermissionSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := permissionSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdPermissionSpec, err = NewPermissionSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdPermissionSpec, nil 62 | } 63 | 64 | func (svc PermissionService) GetByPermissionId(ctx context.Context, permissionId string) (*PermissionSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypePermission, permissionId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewPermissionSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc PermissionService) List(ctx context.Context, listParams service.ListParams) ([]PermissionSpec, error) { 74 | permissionSpecs := make([]PermissionSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypePermission}, listParams) 76 | if err != nil { 77 | return permissionSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | permissionSpec, err := NewPermissionSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return permissionSpecs, err 84 | } 85 | 86 | permissionSpecs = append(permissionSpecs, *permissionSpec) 87 | } 88 | 89 | return permissionSpecs, nil 90 | } 91 | 92 | func (svc PermissionService) UpdateByPermissionId(ctx context.Context, permissionId string, permissionSpec UpdatePermissionSpec) (*PermissionSpec, error) { 93 | var updatedPermissionSpec *PermissionSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypePermission, permissionId, *permissionSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedPermissionSpec, err = NewPermissionSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedPermissionSpec, nil 112 | } 113 | 114 | func (svc PermissionService) DeleteByPermissionId(ctx context.Context, permissionId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypePermission, permissionId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/object/feature/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc FeatureService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/features", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/features", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[FeatureListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/features/{featureId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // update 51 | service.WarrantRoute{ 52 | Pattern: "/v1/features/{featureId}", 53 | Method: "POST", 54 | Handler: service.NewRouteHandler(svc, updateHandler), 55 | }, 56 | service.WarrantRoute{ 57 | Pattern: "/v1/features/{featureId}", 58 | Method: "PUT", 59 | Handler: service.NewRouteHandler(svc, updateHandler), 60 | }, 61 | 62 | // delete 63 | service.WarrantRoute{ 64 | Pattern: "/v1/features/{featureId}", 65 | Method: "DELETE", 66 | Handler: service.NewRouteHandler(svc, deleteHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc FeatureService, w http.ResponseWriter, r *http.Request) error { 72 | var newFeature FeatureSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &newFeature) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdFeature, err := svc.Create(r.Context(), newFeature) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdFeature) 84 | return nil 85 | } 86 | 87 | func getHandler(svc FeatureService, w http.ResponseWriter, r *http.Request) error { 88 | featureId := mux.Vars(r)["featureId"] 89 | feature, err := svc.GetByFeatureId(r.Context(), featureId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, feature) 95 | return nil 96 | } 97 | 98 | func listHandler(svc FeatureService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[FeatureListParamParser](r.Context()) 100 | features, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, features) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc FeatureService, w http.ResponseWriter, r *http.Request) error { 110 | var updateFeature UpdateFeatureSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updateFeature) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | featureId := mux.Vars(r)["featureId"] 117 | updatedFeature, err := svc.UpdateByFeatureId(r.Context(), featureId, updateFeature) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedFeature) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc FeatureService, w http.ResponseWriter, r *http.Request) error { 127 | featureId := mux.Vars(r)["featureId"] 128 | if featureId == "" { 129 | return service.NewMissingRequiredParameterError("featureId") 130 | } 131 | 132 | err := svc.DeleteByFeatureId(r.Context(), featureId) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/object/pricingtier/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "context" 19 | 20 | objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" 21 | object "github.com/warrant-dev/warrant/pkg/object" 22 | "github.com/warrant-dev/warrant/pkg/service" 23 | ) 24 | 25 | type PricingTierService struct { 26 | service.BaseService 27 | objectSvc object.Service 28 | } 29 | 30 | func NewService(env service.Env, objectSvc object.Service) *PricingTierService { 31 | return &PricingTierService{ 32 | BaseService: service.NewBaseService(env), 33 | objectSvc: objectSvc, 34 | } 35 | } 36 | 37 | func (svc PricingTierService) Create(ctx context.Context, pricingTierSpec PricingTierSpec) (*PricingTierSpec, error) { 38 | var createdPricingTierSpec *PricingTierSpec 39 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 40 | objectSpec, err := pricingTierSpec.ToCreateObjectSpec() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | createdObjectSpec, err := svc.objectSvc.Create(txCtx, *objectSpec) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | createdPricingTierSpec, err = NewPricingTierSpecFromObjectSpec(createdObjectSpec) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return createdPricingTierSpec, nil 62 | } 63 | 64 | func (svc PricingTierService) GetByPricingTierId(ctx context.Context, pricingTierId string) (*PricingTierSpec, error) { 65 | objectSpec, err := svc.objectSvc.GetByObjectTypeAndId(ctx, objecttype.ObjectTypePricingTier, pricingTierId) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return NewPricingTierSpecFromObjectSpec(objectSpec) 71 | } 72 | 73 | func (svc PricingTierService) List(ctx context.Context, listParams service.ListParams) ([]PricingTierSpec, error) { 74 | pricingTierSpecs := make([]PricingTierSpec, 0) 75 | objectSpecs, _, _, err := svc.objectSvc.List(ctx, &object.FilterOptions{ObjectType: objecttype.ObjectTypePricingTier}, listParams) 76 | if err != nil { 77 | return pricingTierSpecs, err 78 | } 79 | 80 | for i := range objectSpecs { 81 | pricingTierSpec, err := NewPricingTierSpecFromObjectSpec(&objectSpecs[i]) 82 | if err != nil { 83 | return pricingTierSpecs, err 84 | } 85 | 86 | pricingTierSpecs = append(pricingTierSpecs, *pricingTierSpec) 87 | } 88 | 89 | return pricingTierSpecs, nil 90 | } 91 | 92 | func (svc PricingTierService) UpdateByPricingTierId(ctx context.Context, pricingTierId string, pricingTierSpec UpdatePricingTierSpec) (*PricingTierSpec, error) { 93 | var updatedPricingTierSpec *PricingTierSpec 94 | err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { 95 | updatedObjectSpec, err := svc.objectSvc.UpdateByObjectTypeAndId(txCtx, objecttype.ObjectTypePricingTier, pricingTierId, *pricingTierSpec.ToUpdateObjectSpec()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | updatedPricingTierSpec, err = NewPricingTierSpecFromObjectSpec(updatedObjectSpec) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | }) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return updatedPricingTierSpec, nil 112 | } 113 | 114 | func (svc PricingTierService) DeleteByPricingTierId(ctx context.Context, pricingTierId string) error { 115 | _, err := svc.objectSvc.DeleteByObjectTypeAndId(ctx, objecttype.ObjectTypePricingTier, pricingTierId) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/object/permission/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc PermissionService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/permissions", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/permissions", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[PermissionListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/permissions/{permissionId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // update 51 | service.WarrantRoute{ 52 | Pattern: "/v1/permissions/{permissionId}", 53 | Method: "POST", 54 | Handler: service.NewRouteHandler(svc, updateHandler), 55 | }, 56 | service.WarrantRoute{ 57 | Pattern: "/v1/permissions/{permissionId}", 58 | Method: "PUT", 59 | Handler: service.NewRouteHandler(svc, updateHandler), 60 | }, 61 | 62 | // delete 63 | service.WarrantRoute{ 64 | Pattern: "/v1/permissions/{permissionId}", 65 | Method: "DELETE", 66 | Handler: service.NewRouteHandler(svc, deleteHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc PermissionService, w http.ResponseWriter, r *http.Request) error { 72 | var newPermission PermissionSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &newPermission) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdPermission, err := svc.Create(r.Context(), newPermission) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdPermission) 84 | return nil 85 | } 86 | 87 | func getHandler(svc PermissionService, w http.ResponseWriter, r *http.Request) error { 88 | permissionId := mux.Vars(r)["permissionId"] 89 | permission, err := svc.GetByPermissionId(r.Context(), permissionId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, permission) 95 | return nil 96 | } 97 | 98 | func listHandler(svc PermissionService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[PermissionListParamParser](r.Context()) 100 | permissions, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, permissions) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc PermissionService, w http.ResponseWriter, r *http.Request) error { 110 | var updatePermission UpdatePermissionSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updatePermission) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | permissionId := mux.Vars(r)["permissionId"] 117 | updatedPermission, err := svc.UpdateByPermissionId(r.Context(), permissionId, updatePermission) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedPermission) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc PermissionService, w http.ResponseWriter, r *http.Request) error { 127 | permissionId := mux.Vars(r)["permissionId"] 128 | if permissionId == "" { 129 | return service.NewMissingRequiredParameterError("permissionId") 130 | } 131 | 132 | err := svc.DeleteByPermissionId(r.Context(), permissionId) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/object/pricingtier/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 WorkOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package object 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/warrant-dev/warrant/pkg/service" 22 | ) 23 | 24 | func (svc PricingTierService) Routes() ([]service.Route, error) { 25 | return []service.Route{ 26 | // create 27 | service.WarrantRoute{ 28 | Pattern: "/v1/pricing-tiers", 29 | Method: "POST", 30 | Handler: service.NewRouteHandler(svc, createHandler), 31 | }, 32 | 33 | // list 34 | service.WarrantRoute{ 35 | Pattern: "/v1/pricing-tiers", 36 | Method: "GET", 37 | Handler: service.ChainMiddleware( 38 | service.NewRouteHandler(svc, listHandler), 39 | service.ListMiddleware[PricingTierListParamParser], 40 | ), 41 | }, 42 | 43 | // get 44 | service.WarrantRoute{ 45 | Pattern: "/v1/pricing-tiers/{pricingTierId}", 46 | Method: "GET", 47 | Handler: service.NewRouteHandler(svc, getHandler), 48 | }, 49 | 50 | // update 51 | service.WarrantRoute{ 52 | Pattern: "/v1/pricing-tiers/{pricingTierId}", 53 | Method: "POST", 54 | Handler: service.NewRouteHandler(svc, updateHandler), 55 | }, 56 | service.WarrantRoute{ 57 | Pattern: "/v1/pricing-tiers/{pricingTierId}", 58 | Method: "PUT", 59 | Handler: service.NewRouteHandler(svc, updateHandler), 60 | }, 61 | 62 | // delete 63 | service.WarrantRoute{ 64 | Pattern: "/v1/pricing-tiers/{pricingTierId}", 65 | Method: "DELETE", 66 | Handler: service.NewRouteHandler(svc, deleteHandler), 67 | }, 68 | }, nil 69 | } 70 | 71 | func createHandler(svc PricingTierService, w http.ResponseWriter, r *http.Request) error { 72 | var newPricingTier PricingTierSpec 73 | err := service.ParseJSONBody(r.Context(), r.Body, &newPricingTier) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | createdPricingTier, err := svc.Create(r.Context(), newPricingTier) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | service.SendJSONResponse(w, createdPricingTier) 84 | return nil 85 | } 86 | 87 | func getHandler(svc PricingTierService, w http.ResponseWriter, r *http.Request) error { 88 | pricingTierId := mux.Vars(r)["pricingTierId"] 89 | pricingTier, err := svc.GetByPricingTierId(r.Context(), pricingTierId) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | service.SendJSONResponse(w, pricingTier) 95 | return nil 96 | } 97 | 98 | func listHandler(svc PricingTierService, w http.ResponseWriter, r *http.Request) error { 99 | listParams := service.GetListParamsFromContext[PricingTierListParamParser](r.Context()) 100 | pricingTiers, err := svc.List(r.Context(), listParams) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | service.SendJSONResponse(w, pricingTiers) 106 | return nil 107 | } 108 | 109 | func updateHandler(svc PricingTierService, w http.ResponseWriter, r *http.Request) error { 110 | var updatePricingTier UpdatePricingTierSpec 111 | err := service.ParseJSONBody(r.Context(), r.Body, &updatePricingTier) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | pricingTierId := mux.Vars(r)["pricingTierId"] 117 | updatedPricingTier, err := svc.UpdateByPricingTierId(r.Context(), pricingTierId, updatePricingTier) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | service.SendJSONResponse(w, updatedPricingTier) 123 | return nil 124 | } 125 | 126 | func deleteHandler(svc PricingTierService, w http.ResponseWriter, r *http.Request) error { 127 | pricingTierId := mux.Vars(r)["pricingTierId"] 128 | if pricingTierId == "" { 129 | return service.NewMissingRequiredParameterError("pricingTierId") 130 | } 131 | 132 | err := svc.DeleteByPricingTierId(r.Context(), pricingTierId) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | --------------------------------------------------------------------------------