├── migrations ├── .gitkeep ├── 2022-06-23-231316_approvals │ ├── down.sql │ └── up.sql ├── 2022-06-23-213746_gcp_requests │ ├── down.sql │ └── up.sql ├── 2022-06-24-232044_rejections │ ├── down.sql │ └── up.sql ├── 2022-06-29-155026_aws_requests │ ├── down.sql │ └── up.sql ├── 2022-07-15-143857_extensions │ ├── down.sql │ └── up.sql ├── 2022-07-15-160903_revocations │ ├── down.sql │ └── up.sql ├── 2022-06-24-232117_cancellations │ ├── down.sql │ └── up.sql ├── 2022-07-29-150317_user_aliases │ ├── down.sql │ └── up.sql ├── 2022-06-29-025216_justifications │ ├── down.sql │ └── up.sql ├── 2022-08-02-182633_platform_tokens │ ├── down.sql │ └── up.sql ├── 2022-06-23-204541_create_access_requests │ ├── down.sql │ └── up.sql ├── 2022-06-29-043057_cf_requests │ ├── down.sql │ └── up.sql ├── 2022-09-02-030705_slack │ ├── down.sql │ └── up.sql └── 2022-06-22-183332_companies │ └── down.sql ├── satounki-go ├── go.mod └── satounki.go ├── satounki-platform-go ├── go.mod └── satounki.go ├── assets ├── ui.png └── slackbot.png ├── rustfmt.toml ├── terraform-providers ├── satounki │ ├── CHANGELOG.md │ ├── examples │ │ ├── provider │ │ │ └── provider.tf │ │ ├── data-sources │ │ │ └── scaffolding_example │ │ │ │ └── data-source.tf │ │ ├── resources │ │ │ └── scaffolding_example │ │ │ │ └── resource.tf │ │ └── README.md │ ├── terraform-registry-manifest.json │ ├── GNUmakefile │ ├── tools │ │ └── tools.go │ ├── internal │ │ └── provider │ │ │ ├── utils.go │ │ │ ├── user_roles_resource_data.go │ │ │ ├── cloudflare_account_resource_data.go │ │ │ ├── user_aliases_resource_data.go │ │ │ ├── aws_account_resource_data.go │ │ │ ├── user_roles_resource.go │ │ │ ├── gcp_project_resource_data.go │ │ │ ├── aws_account_resource.go │ │ │ ├── gcp_project_resource.go │ │ │ ├── cloudflare_account_resource.go │ │ │ ├── policy_resource_data.go │ │ │ ├── user_aliases_resource.go │ │ │ ├── user_roles_model.go │ │ │ ├── policy_resource.go │ │ │ ├── aws_account_model.go │ │ │ ├── gcp_project_model.go │ │ │ ├── cloudflare_account_model.go │ │ │ └── user_aliases_model.go │ ├── .golangci.yml │ ├── docs │ │ ├── index.md │ │ └── resources │ │ │ ├── user_roles.md │ │ │ ├── user_aliases.md │ │ │ ├── aws_account.md │ │ │ ├── cloudflare_account.md │ │ │ ├── gcp_project.md │ │ │ └── policy.md │ ├── main.go │ └── .goreleaser.yml └── satounkiplatform │ ├── CHANGELOG.md │ ├── examples │ ├── provider │ │ └── provider.tf │ ├── data-sources │ │ └── scaffolding_example │ │ │ └── data-source.tf │ ├── resources │ │ └── scaffolding_example │ │ │ └── resource.tf │ └── README.md │ ├── terraform-registry-manifest.json │ ├── GNUmakefile │ ├── tools │ └── tools.go │ ├── internal │ └── provider │ │ ├── utils.go │ │ ├── company_resource_data.go │ │ ├── company_resource.go │ │ └── company_model.go │ ├── .golangci.yml │ ├── docs │ ├── index.md │ └── resources │ │ └── company.md │ ├── main.go │ └── .goreleaser.yml ├── satounki-ts └── package.json ├── rust-toolchain.toml ├── .sops.yaml ├── client ├── src │ ├── cli.rs │ ├── configuration.yaml │ ├── configuration.rs │ └── main.rs └── Cargo.toml ├── common-gen ├── src │ ├── typescript │ │ ├── get_list.rs │ │ ├── put.rs │ │ ├── delete.rs │ │ ├── patch_id.rs │ │ ├── get_id.rs │ │ ├── put_id.rs │ │ ├── post.rs │ │ ├── patch_id_body.rs │ │ ├── post_id.rs │ │ ├── put_id_body.rs │ │ └── mod.rs │ ├── golang │ │ ├── delete.rs │ │ ├── patch_id.rs │ │ ├── patch_id_body.rs │ │ ├── get_list.rs │ │ ├── get_id.rs │ │ ├── put.rs │ │ ├── put_id.rs │ │ ├── post.rs │ │ ├── post_id.rs │ │ └── put_id_body.rs │ └── terraform.rs └── Cargo.toml ├── gcloud ├── src │ ├── error.rs │ └── lib.rs └── Cargo.toml ├── common-platform ├── src │ ├── error_response.rs │ ├── platform_token.rs │ ├── platform_token_scope.rs │ ├── lib.rs │ └── company.rs └── Cargo.toml ├── diesel.toml ├── .gitignore ├── common ├── src │ ├── error_response.rs │ ├── permissions_action.rs │ ├── user_token.rs │ ├── user_status.rs │ ├── requests.rs │ ├── user_roles.rs │ ├── user_aliases.rs │ ├── access_request_state.rs │ ├── lib.rs │ ├── request_policy.rs │ └── policy.rs └── Cargo.toml ├── aws └── Cargo.toml ├── slack ├── Cargo.toml └── src │ ├── users.rs │ └── lib.rs ├── cloudflare ├── Cargo.toml └── src │ ├── lib.rs │ └── tokens.rs ├── api ├── src │ ├── token_validator.rs │ ├── auth │ │ ├── mod.rs │ │ ├── access_role.rs │ │ ├── platform_token_scope.rs │ │ ├── individual_user.rs │ │ ├── api_token.rs │ │ ├── platform_token_with_scope.rs │ │ ├── api_token_or_user_with_access_role.rs │ │ └── user_with_access_role.rs │ ├── rolescraper.rs │ ├── worker │ │ ├── mod.rs │ │ └── server.rs │ ├── settings_gcp_projects.rs │ ├── settings_aws_accounts.rs │ ├── settings_cf_accounts.rs │ ├── policies.rs │ ├── requests.rs │ ├── user_token.rs │ ├── settings_token.rs │ ├── platform_token.rs │ └── platform_doc.rs ├── Cargo.toml └── templates │ ├── policies.html │ ├── base.html │ ├── settings.html │ └── users.html ├── .envrc ├── .github └── workflows │ └── nix.yml ├── rolescraper ├── Cargo.toml └── src │ └── main.rs ├── common-macros └── Cargo.toml ├── cli ├── Cargo.toml └── src │ └── reporters.rs ├── database ├── Cargo.toml └── src │ ├── company_role.rs │ ├── justification.rs │ ├── rejection.rs │ ├── request_slack.rs │ ├── approval.rs │ ├── revocation.rs │ ├── extension.rs │ ├── cancellation.rs │ ├── user_company.rs │ ├── user_token.rs │ ├── api_token.rs │ ├── user_company_role.rs │ ├── worker_key.rs │ ├── company_policy.rs │ ├── gcp_request.rs │ ├── aws_request.rs │ ├── company_slack.rs │ ├── cloudflare_request.rs │ ├── user_alias.rs │ ├── platform_token.rs │ ├── company_gcp_project.rs │ ├── company_aws_account.rs │ └── company_cloudflare_account.rs ├── dev_data.sql └── Cargo.toml /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /satounki-go/go.mod: -------------------------------------------------------------------------------- 1 | module satounki 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /satounki-platform-go/go.mod: -------------------------------------------------------------------------------- 1 | module satounki-platform 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /assets/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LGUG2Z/satounki/HEAD/assets/ui.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Item" 2 | group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /terraform-providers/satounki/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (Unreleased) 2 | 3 | FEATURES: 4 | -------------------------------------------------------------------------------- /assets/slackbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LGUG2Z/satounki/HEAD/assets/slackbot.png -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (Unreleased) 2 | 3 | FEATURES: 4 | -------------------------------------------------------------------------------- /satounki-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "typed-rest-client": "^1.8.9" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /migrations/2022-06-23-231316_approvals/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE approvals -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "1.74.0" 4 | components = ["rust-analyzer"] 5 | -------------------------------------------------------------------------------- /migrations/2022-06-23-213746_gcp_requests/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE gcp_requests -------------------------------------------------------------------------------- /migrations/2022-06-24-232044_rejections/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE rejections -------------------------------------------------------------------------------- /migrations/2022-06-29-155026_aws_requests/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE aws_requests -------------------------------------------------------------------------------- /migrations/2022-07-15-143857_extensions/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE extensions; -------------------------------------------------------------------------------- /migrations/2022-07-15-160903_revocations/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE revocations; -------------------------------------------------------------------------------- /terraform-providers/satounki/examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "scaffolding" { 2 | # example configuration here 3 | } 4 | -------------------------------------------------------------------------------- /migrations/2022-06-24-232117_cancellations/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE cancellations -------------------------------------------------------------------------------- /migrations/2022-07-29-150317_user_aliases/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE user_aliases; -------------------------------------------------------------------------------- /migrations/2022-06-29-025216_justifications/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE justifications 4 | -------------------------------------------------------------------------------- /migrations/2022-08-02-182633_platform_tokens/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE platform_tokens; -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "scaffolding" { 2 | # example configuration here 3 | } 4 | -------------------------------------------------------------------------------- /migrations/2022-06-23-204541_create_access_requests/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE access_requests -------------------------------------------------------------------------------- /migrations/2022-06-29-043057_cf_requests/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE cloudflare_requests; 4 | -------------------------------------------------------------------------------- /migrations/2022-09-02-030705_slack/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE company_slack; 4 | DROP TABLE requests_slack; -------------------------------------------------------------------------------- /terraform-providers/satounki/terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /terraform-providers/satounki/examples/data-sources/scaffolding_example/data-source.tf: -------------------------------------------------------------------------------- 1 | data "scaffolding_example" "example" { 2 | configurable_attribute = "some-value" 3 | } 4 | -------------------------------------------------------------------------------- /terraform-providers/satounki/examples/resources/scaffolding_example/resource.tf: -------------------------------------------------------------------------------- 1 | resource "scaffolding_example" "example" { 2 | configurable_attribute = "some-value" 3 | } 4 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /terraform-providers/satounki/GNUmakefile: -------------------------------------------------------------------------------- 1 | default: testacc 2 | 3 | # Run acceptance tests 4 | .PHONY: testacc 5 | testacc: 6 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 7 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/examples/data-sources/scaffolding_example/data-source.tf: -------------------------------------------------------------------------------- 1 | data "scaffolding_example" "example" { 2 | configurable_attribute = "some-value" 3 | } 4 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/examples/resources/scaffolding_example/resource.tf: -------------------------------------------------------------------------------- 1 | resource "scaffolding_example" "example" { 2 | configurable_attribute = "some-value" 3 | } 4 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/GNUmakefile: -------------------------------------------------------------------------------- 1 | default: testacc 2 | 3 | # Run acceptance tests 4 | .PHONY: testacc 5 | testacc: 6 | TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 7 | -------------------------------------------------------------------------------- /.sops.yaml: -------------------------------------------------------------------------------- 1 | keys: 2 | - &LGUG2Z age1hjclc9mgzpuezelu92v3a7jn96gchuvzmdprzy9q722l62853s4qw5xfuu 3 | creation_rules: 4 | - path_regex: ^secrets.yaml$ 5 | key_groups: 6 | - age: 7 | - *LGUG2Z 8 | -------------------------------------------------------------------------------- /migrations/2022-08-02-182633_platform_tokens/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE platform_tokens 4 | ( 5 | token TEXT PRIMARY KEY UNIQUE NOT NULL, 6 | scope TEXT UNIQUE NOT NULL 7 | ); -------------------------------------------------------------------------------- /client/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Parser, Debug)] 6 | #[clap(version, long_about = None)] 7 | pub struct Cli { 8 | #[clap(short, long)] 9 | pub config: PathBuf, 10 | } 11 | -------------------------------------------------------------------------------- /common-gen/src/typescript/get_list.rs: -------------------------------------------------------------------------------- 1 | pub const GET_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Get(): Promise> { 3 | return await this.client.get("/v1{{ get }}"); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /gcloud/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error(transparent)] 6 | Io(#[from] std::io::Error), 7 | #[error(transparent)] 8 | Serialization(#[from] serde_yaml::Error), 9 | } 10 | -------------------------------------------------------------------------------- /common-gen/src/typescript/put.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Put(): Promise> { 3 | return await this.client.replace("/v1{{ put }}", null); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common-gen/src/typescript/delete.rs: -------------------------------------------------------------------------------- 1 | pub const DELETE_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Delete(id: string): Promise> { 3 | return await this.client.del(`/v1{{ delete|replace("%s", "${id}") }}`); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common-gen/src/typescript/patch_id.rs: -------------------------------------------------------------------------------- 1 | pub const PATCH_ID_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Patch(id: string): Promise> { 3 | return await this.client.update(`/v1{{ patch|replace("%s", "${id}") }}`, {}); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common-gen/src/typescript/get_id.rs: -------------------------------------------------------------------------------- 1 | pub const GET_ID_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Get(id: string): Promise> { 3 | return await this.client.get(`/v1{{ get|replace("%s", "${id}") }}`); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common-platform/src/error_response.rs: -------------------------------------------------------------------------------- 1 | use crate::Schema; 2 | 3 | /// Error returned by the Satounki API 4 | #[apply(Schema!)] 5 | pub struct ErrorResponse { 6 | /// HTTP error code 7 | pub code: u16, 8 | /// User-friendly error message 9 | pub error: String, 10 | } 11 | -------------------------------------------------------------------------------- /common-gen/src/typescript/put_id.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_ID_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Put(id: string): Promise> { 3 | return await this.client.replace(`/v1{{ put|replace("%s", "${id}") }}`); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common-gen/src/typescript/post.rs: -------------------------------------------------------------------------------- 1 | pub const POST_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Post(body: satounki.{{ name}}PostRequest): Promise> { 3 | return await this.client.create("/v1{{ post }}", body); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /terraform-providers/satounki/tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | 6 | package tools 7 | 8 | import ( 9 | // Documentation generation 10 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 11 | ) 12 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | 6 | package tools 7 | 8 | import ( 9 | // Documentation generation 10 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 11 | ) 12 | -------------------------------------------------------------------------------- /client/src/configuration.yaml: -------------------------------------------------------------------------------- 1 | aws: 2 | dev: 3 | aws_access_key_id: bla 4 | aws_secret_access_key: bla 5 | canary: 6 | aws_access_key_id: bla 7 | aws_secret_access_key: bla 8 | prod: 9 | aws_access_key_id: bla 10 | aws_secret_access_key: bla 11 | gcp: ~/credentials.json 12 | cf: token -------------------------------------------------------------------------------- /common-gen/src/typescript/patch_id_body.rs: -------------------------------------------------------------------------------- 1 | pub const PATCH_ID_BODY_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Patch(id: string, body: satounki.{{ name}}PatchRequest): Promise> { 3 | return await this.client.update(`/v1{{ patch|replace("%s", "${id}") }}`, body); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "database/src/schema.gen.rs" 6 | import_types = [ 7 | "diesel::sql_types::*", 8 | "common::AccessRequestStateMapping", 9 | "common::CloudflareRoleMapping" 10 | ] 11 | -------------------------------------------------------------------------------- /migrations/2022-07-29-150317_user_aliases/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE user_aliases 3 | ( 4 | user_id INTEGER NOT NULL, 5 | aws TEXT, 6 | cloudflare TEXT, 7 | gcp TEXT, 8 | 9 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, 10 | PRIMARY KEY (user_id) 11 | ); 12 | -------------------------------------------------------------------------------- /common-gen/src/typescript/post_id.rs: -------------------------------------------------------------------------------- 1 | pub const POST_ID_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Post(id: string, body: satounki.{{ name}}PostRequest): Promise> { 3 | return await this.client.create(`/v1{{ post|replace("%s", "${id}") }}`, body); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | json-* 3 | target 4 | node_modules 5 | vendor 6 | .terraform 7 | .ideaDataSources 8 | *.db 9 | *.sqlite 10 | *.iml 11 | *.ipr 12 | *.iws 13 | *.tfstate* 14 | .idea 15 | .direnv/ 16 | .env 17 | dev.db* 18 | process-compose.yaml 19 | tunnel-credentials.json 20 | tunnel-config.yaml 21 | .terraform.lock.hcl 22 | result 23 | -------------------------------------------------------------------------------- /common-gen/src/typescript/put_id_body.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_ID_BODY_TEMPLATE_TS: &str = r#" 2 | async {{ name|tsify }}Put(id: string, body: satounki.{{ name}}PutRequest): Promise> { 3 | return await this.client.replace(`/v1{{ put|replace("%s", "${id}") }}`, body); 4 | } 5 | "#; 6 | -------------------------------------------------------------------------------- /common/src/error_response.rs: -------------------------------------------------------------------------------- 1 | use utoipa::ToResponse; 2 | 3 | use crate::Schema; 4 | 5 | /// Error returned by the Satounki API 6 | #[apply(Schema!)] 7 | #[derive(ToResponse)] 8 | pub struct ErrorResponse { 9 | /// HTTP error code 10 | pub code: u16, 11 | /// User-friendly error message 12 | pub error: String, 13 | } 14 | -------------------------------------------------------------------------------- /migrations/2022-06-29-025216_justifications/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE justifications 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | justification TEXT NOT NULL, 7 | 8 | FOREIGN KEY (access_request_id) 9 | REFERENCES access_requests (id) ON DELETE CASCADE, 10 | 11 | PRIMARY KEY (access_request_id) 12 | ) -------------------------------------------------------------------------------- /aws/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aws" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | aws-sdk-iam = { version = "1" } 10 | aws-config = { version = "1", features = ["behavior-version-latest"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /slack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slack" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { workspace = true } 10 | serde = { workspace = true } 11 | serde_json = { workspace = true } 12 | serde_with = "3" 13 | slack-blocks = "0.25" -------------------------------------------------------------------------------- /cloudflare/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cloudflare" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { workspace = true } 10 | serde = { workspace = true } 11 | serde_json = { workspace = true } 12 | common = { path = "../common" } 13 | -------------------------------------------------------------------------------- /common/src/permissions_action.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | /// Action to be taken with a set of permissions 5 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 6 | pub enum PermissionsAction { 7 | /// Grant permissions to the requesting user 8 | Add, 9 | /// Remove permissions from the requesting user 10 | Remove, 11 | } 12 | -------------------------------------------------------------------------------- /gcloud/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::nursery, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc, clippy::use_self)] 3 | 4 | pub use error::Error; 5 | pub use gcloud::Gcloud; 6 | pub use gcloud::Inputs as GcloudInputData; 7 | pub use iam_policy::IamPolicy; 8 | pub use iam_policy::Wrapper as IamPolicyWrapper; 9 | 10 | mod error; 11 | mod gcloud; 12 | mod iam_policy; 13 | -------------------------------------------------------------------------------- /api/src/token_validator.rs: -------------------------------------------------------------------------------- 1 | use actix_web::dev::ServiceRequest; 2 | use actix_web::HttpMessage; 3 | use actix_web_httpauth::extractors::bearer::BearerAuth; 4 | 5 | pub async fn validator( 6 | req: ServiceRequest, 7 | credentials: BearerAuth, 8 | ) -> Result { 9 | req.extensions_mut().insert(credentials); 10 | Ok(req) 11 | } 12 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export NIXPKGS_ALLOW_UNFREE=1 2 | unset LD_LIBRARY_PATH 3 | # impure set to allow use of NIXPKGS_ALLOW_UNFREE (terraform) 4 | use flake . --impure 5 | 6 | if test -f ~/.ssh/id_ed25519; then 7 | export SOPS_AGE_KEY=$(ssh-to-age -i ~/.ssh/id_ed25519 -private-key) 8 | fi 9 | 10 | if test -f .env; then 11 | set -a 12 | source .env 13 | set +a 14 | fi 15 | 16 | export COMPANY_DOMAIN=satounki.com 17 | -------------------------------------------------------------------------------- /migrations/2022-06-23-231316_approvals/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE approvals 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | user TEXT NOT NULL REFERENCES users (email), 7 | timestamp TEXT NOT NULL, 8 | 9 | FOREIGN KEY (access_request_id) 10 | REFERENCES access_requests (id) ON DELETE CASCADE, 11 | 12 | PRIMARY KEY (access_request_id, user) 13 | ) -------------------------------------------------------------------------------- /migrations/2022-06-24-232044_rejections/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE rejections 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | user TEXT NOT NULL REFERENCES users (email), 7 | timestamp TEXT NOT NULL, 8 | 9 | FOREIGN KEY (access_request_id) 10 | REFERENCES access_requests (id) ON DELETE CASCADE, 11 | 12 | PRIMARY KEY (access_request_id) 13 | ) 14 | -------------------------------------------------------------------------------- /migrations/2022-06-24-232117_cancellations/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE cancellations 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | user TEXT NOT NULL REFERENCES users (email), 7 | timestamp TEXT NOT NULL, 8 | 9 | FOREIGN KEY (access_request_id) 10 | REFERENCES access_requests (id) ON DELETE CASCADE, 11 | PRIMARY KEY (access_request_id) 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /migrations/2022-07-15-160903_revocations/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE revocations 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | user TEXT NOT NULL REFERENCES users (email), 7 | timestamp TEXT NOT NULL, 8 | 9 | FOREIGN KEY (access_request_id) 10 | REFERENCES access_requests (id) ON DELETE CASCADE, 11 | 12 | PRIMARY KEY (access_request_id) 13 | ) 14 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Nix 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: DeterminateSystems/nix-installer-action@main 13 | - uses: DeterminateSystems/magic-nix-cache-action@main 14 | with: 15 | diagnostic-endpoint: "" 16 | - run: nix build . 17 | -------------------------------------------------------------------------------- /common/src/user_token.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | use crate::Schema; 4 | 5 | /// User API token for personal use 6 | #[apply(Schema!)] 7 | pub struct UserToken { 8 | /// Token 9 | #[schema(example = "super-duper-secret-user-token")] 10 | pub token: String, 11 | } 12 | 13 | route_request_response! { 14 | #[Get] UserToken() -> UserToken, 15 | #[Put] UserToken() -> UserToken, 16 | } 17 | -------------------------------------------------------------------------------- /gcloud/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gcloud" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = { workspace = true } 10 | derive_more = { workspace = true } 11 | serde = { workspace = true } 12 | serde_yaml = { workspace = true } 13 | thiserror = { workspace = true } 14 | common = { path = "../common" } -------------------------------------------------------------------------------- /rolescraper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rolescraper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | gcloud = { path = "../gcloud" } 10 | aws = { path = "../aws" } 11 | color-eyre = { workspace = true } 12 | tokio = { workspace = true } 13 | serde_json = { workspace = true } 14 | serde = { workspace = true } -------------------------------------------------------------------------------- /common-platform/src/platform_token.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | /// Platform API token used for automation 4 | #[apply(crate::Schema!)] 5 | pub struct PlatformToken { 6 | /// Token 7 | #[schema(example = "super-duper-secret-platform-token")] 8 | pub token: String, 9 | } 10 | 11 | route_request_response! { 12 | #[Get] PlatformToken() -> PlatformToken, 13 | #[Put] PlatformToken() -> PlatformToken, 14 | } 15 | -------------------------------------------------------------------------------- /migrations/2022-07-15-143857_extensions/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE extensions 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | user TEXT NOT NULL REFERENCES users (email), 7 | timestamp TEXT NOT NULL, 8 | duration INTEGER NOT NULL, 9 | 10 | FOREIGN KEY (access_request_id) 11 | REFERENCES access_requests (id) ON DELETE CASCADE, 12 | 13 | PRIMARY KEY (access_request_id, user) 14 | ) 15 | -------------------------------------------------------------------------------- /migrations/2022-06-22-183332_companies/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE users; 4 | DROP TABLE companies; 5 | DROP TABLE company_roles; 6 | DROP TABLE users_companies; 7 | DROP TABLE users_companies_roles; 8 | DROP TABLE company_policies; 9 | DROP TABLE company_aws_accounts; 10 | DROP TABLE company_cloudflare_accounts; 11 | DROP TABLE company_gcp_projects; 12 | DROP TABLE worker_keys; 13 | DROP TABLE api_tokens; 14 | DROP TABLE user_tokens; 15 | -------------------------------------------------------------------------------- /api/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub use access_role::*; 2 | pub use api_token::*; 3 | pub use api_token_or_user_with_access_role::*; 4 | pub use individual_user::*; 5 | pub use platform_token_scope::*; 6 | pub use platform_token_with_scope::*; 7 | pub use user_with_access_role::*; 8 | 9 | mod access_role; 10 | mod api_token; 11 | mod api_token_or_user_with_access_role; 12 | mod authenticated; 13 | mod individual_user; 14 | mod platform_token_scope; 15 | mod platform_token_with_scope; 16 | mod user_with_access_role; 17 | -------------------------------------------------------------------------------- /migrations/2022-06-29-043057_cf_requests/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE cloudflare_requests 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | company_id INTEGER NOT NULL, 7 | user TEXT NOT NULL, 8 | account_alias TEXT NOT NULL, 9 | role TEXT NOT NULL, 10 | 11 | FOREIGN KEY (access_request_id) 12 | REFERENCES access_requests (id) ON DELETE CASCADE, 13 | 14 | PRIMARY KEY (access_request_id, user, role) 15 | ) 16 | -------------------------------------------------------------------------------- /common-platform/src/platform_token_scope.rs: -------------------------------------------------------------------------------- 1 | use diesel_derive_enum::DbEnum; 2 | use serde::Deserialize; 3 | use serde::Serialize; 4 | use strum::EnumString; 5 | 6 | /// Scopes of a Platform token 7 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, DbEnum, EnumString)] 8 | #[serde(rename_all = "snake_case")] 9 | #[strum(serialize_all = "snake_case")] 10 | pub enum PlatformTokenScope { 11 | /// View Platform resources 12 | Read, 13 | /// Edit Platform resources 14 | Write, 15 | } 16 | -------------------------------------------------------------------------------- /common-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common-macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = { workspace = true } 10 | derive_more = { workspace = true } 11 | display_json = { workspace = true } 12 | paste = { workspace = true } 13 | schemars = { workspace = true } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | utoipa = { workspace = true } -------------------------------------------------------------------------------- /common-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common-gen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common = { path = "../common" } 10 | common-platform = { path = "../common-platform" } 11 | 12 | convert_case = "0.6" 13 | anyhow = "1" 14 | minijinja = "1" 15 | paste = { workspace = true } 16 | serde = { workspace = true } 17 | schemars = { workspace = true } 18 | serde_json = { workspace = true } -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func fieldDoc(kind interface{}, field string) string { 8 | t := reflect.TypeOf(kind) 9 | for i := 0; i < t.NumField(); i++ { 10 | tfsdk := t.Field(i).Tag.Get("tfsdk") 11 | if tfsdk == field { 12 | return t.Field(i).Tag.Get("rustdoc") 13 | } 14 | } 15 | 16 | return "" 17 | } 18 | 19 | func resourceDoc(kind interface{}) string { 20 | t := reflect.TypeOf(kind) 21 | return t.Field(0).Tag.Get("resourcedoc") 22 | } 23 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/internal/provider/utils.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func fieldDoc(kind interface{}, field string) string { 8 | t := reflect.TypeOf(kind) 9 | for i := 0; i < t.NumField(); i++ { 10 | tfsdk := t.Field(i).Tag.Get("tfsdk") 11 | if tfsdk == field { 12 | return t.Field(i).Tag.Get("rustdoc") 13 | } 14 | } 15 | 16 | return "" 17 | } 18 | 19 | func resourceDoc(kind interface{}) string { 20 | t := reflect.TypeOf(kind) 21 | return t.Field(0).Tag.Get("resourcedoc") 22 | } 23 | -------------------------------------------------------------------------------- /api/src/rolescraper.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 5 | pub struct Gcp { 6 | pub description: Option, 7 | pub etag: String, 8 | pub name: String, 9 | pub stage: Option, 10 | pub title: Option, 11 | } 12 | 13 | #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 14 | pub struct Aws { 15 | pub path: String, 16 | pub policy_name: String, 17 | pub policy_id: String, 18 | pub arn: String, 19 | } 20 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "satounki" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common = { path = "../common" } 10 | 11 | chrono = { workspace = true } 12 | clap = { workspace = true } 13 | color-eyre = { workspace = true } 14 | edit = "0.1" 15 | reqwest = { workspace = true } 16 | serde = { workspace = true } 17 | serde_json = { workspace = true } 18 | tabled = { version = "0.14", features = ["color"] } 19 | termcolor = "1" 20 | termcolor-json = "1" 21 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/.golangci.yml: -------------------------------------------------------------------------------- 1 | # Visit https://golangci-lint.run/ for usage documentation 2 | # and information on other useful linters 3 | issues: 4 | max-per-linter: 0 5 | max-same-issues: 0 6 | 7 | linters: 8 | disable-all: true 9 | enable: 10 | - durationcheck 11 | - errcheck 12 | - exportloopref 13 | - forcetypeassert 14 | - gofmt 15 | - gosimple 16 | - ineffassign 17 | - makezero 18 | - misspell 19 | - nilerr 20 | - predeclared 21 | - staticcheck 22 | - tenv 23 | - unconvert 24 | - unused 25 | - vet -------------------------------------------------------------------------------- /slack/src/users.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use serde_with::skip_serializing_none; 4 | 5 | use crate::SlackResponse; 6 | 7 | impl SlackResponse for SlackApiUsersProfileGetResponse {} 8 | 9 | #[skip_serializing_none] 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct SlackApiUsersProfileGetResponse { 12 | pub ok: bool, 13 | pub error: Option, 14 | pub profile: SlackUserProfile, 15 | } 16 | 17 | #[skip_serializing_none] 18 | #[derive(Debug, Serialize, Deserialize)] 19 | pub struct SlackUserProfile { 20 | pub email: String, 21 | } 22 | -------------------------------------------------------------------------------- /common/src/user_status.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | use schemars::JsonSchema; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use utoipa::ToSchema; 6 | 7 | use crate::Terraform; 8 | 9 | /// User status 10 | #[apply(Terraform!)] 11 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)] 12 | #[serde(rename_all = "snake_case")] 13 | pub enum UserStatus { 14 | /// User is enabled 15 | Enabled, 16 | /// User is disabled 17 | Disabled, 18 | } 19 | 20 | route_request_response! { 21 | #[Get] UserStatus() -> UserStatus, 22 | } 23 | -------------------------------------------------------------------------------- /terraform-providers/satounki/.golangci.yml: -------------------------------------------------------------------------------- 1 | # Visit https://golangci-lint.run/ for usage documentation 2 | # and information on other useful linters 3 | issues: 4 | max-per-linter: 0 5 | max-same-issues: 0 6 | 7 | linters: 8 | disable-all: true 9 | enable: 10 | - durationcheck 11 | - errcheck 12 | - exportloopref 13 | - forcetypeassert 14 | - gofmt 15 | - gosimple 16 | - ineffassign 17 | - makezero 18 | - misspell 19 | - nilerr 20 | - predeclared 21 | - staticcheck 22 | - tenv 23 | - unconvert 24 | - unused 25 | - unparam 26 | - vet -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki Provider" 4 | subcategory: "" 5 | description: |- 6 | Interact with Satounki 7 | --- 8 | 9 | # satounki Provider 10 | 11 | Interact with Satounki 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | provider "scaffolding" { 17 | # example configuration here 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `api_token` (String, Sensitive) API Token 27 | - `base_url` (String) Satounki API Base URL 28 | -------------------------------------------------------------------------------- /slack/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::nursery, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc, clippy::use_self)] 3 | 4 | pub use chat::Attachment; 5 | pub use chat::SlackApiChatPostMessageRequest; 6 | pub use chat::SlackApiChatUpdateRequest; 7 | pub use chat::SlackMessageContent; 8 | use serde::de::DeserializeOwned; 9 | use serde::Serialize; 10 | pub use slack::Slack; 11 | 12 | mod chat; 13 | mod slack; 14 | mod users; 15 | 16 | type Result = core::result::Result; 17 | 18 | trait SlackRequest: Serialize + Sync + Send {} 19 | trait SlackResponse: DeserializeOwned + Sync + Send {} 20 | -------------------------------------------------------------------------------- /cloudflare/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::nursery, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc, clippy::use_self)] 3 | 4 | pub use cloudflare::Cloudflare; 5 | use member::ListMembersResponse; 6 | use member::Member as UpdateMemberRequest; 7 | use member::Member; 8 | use member::UpdateMemberResponse; 9 | use serde::de::DeserializeOwned; 10 | use serde::Serialize; 11 | 12 | mod cloudflare; 13 | mod member; 14 | mod tokens; 15 | 16 | type Result = core::result::Result; 17 | 18 | trait CloudflareRequest: Serialize + Sync + Send {} 19 | trait CloudflareResponse: DeserializeOwned + Sync + Send {} 20 | -------------------------------------------------------------------------------- /migrations/2022-06-23-213746_gcp_requests/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE gcp_requests 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | company_id INTEGER NOT NULL, 7 | user TEXT NOT NULL, 8 | project TEXT NOT NULL, 9 | role TEXT NOT NULL, 10 | 11 | FOREIGN KEY (access_request_id) 12 | REFERENCES access_requests (id) ON DELETE CASCADE, 13 | 14 | FOREIGN KEY (company_id, project) 15 | REFERENCES company_gcp_projects (company_id, gcp_project) ON DELETE NO ACTION, 16 | 17 | PRIMARY KEY (access_request_id, user, project, role) 18 | ) -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounkiplatform Provider" 4 | subcategory: "" 5 | description: |- 6 | Interact with Satounki's Platform 7 | --- 8 | 9 | # satounkiplatform Provider 10 | 11 | Interact with Satounki's Platform 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | provider "scaffolding" { 17 | # example configuration here 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `api_token` (String, Sensitive) Platform Token 27 | - `base_url` (String) Base URL 28 | -------------------------------------------------------------------------------- /migrations/2022-06-29-155026_aws_requests/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE aws_requests 4 | ( 5 | access_request_id TEXT NOT NULL, 6 | company_id INTEGER NOT NULL, 7 | user TEXT NOT NULL, 8 | account_alias TEXT NOT NULL, 9 | role TEXT NOT NULL, 10 | 11 | FOREIGN KEY (access_request_id) 12 | REFERENCES access_requests (id) ON DELETE CASCADE, 13 | 14 | FOREIGN KEY (company_id, account_alias) 15 | REFERENCES company_aws_accounts (company_id, aws_account_alias) ON DELETE NO ACTION, 16 | 17 | PRIMARY KEY (access_request_id, user, account_alias, role) 18 | ) 19 | -------------------------------------------------------------------------------- /common-platform/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate macro_rules_attribute; 3 | 4 | attribute_alias! { 5 | #[apply(Schema!)] = #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, display_json::DisplayAsJsonPretty, schemars::JsonSchema, utoipa::ToSchema)]; 6 | #[apply(Terraform!)] = #[macro_rules_derive(common_macros::terraform_resource!)]; 7 | #[apply(New!)] = #[macro_rules_derive(common_macros::new_resource!)]; 8 | } 9 | 10 | pub use company::*; 11 | pub use error_response::*; 12 | pub use platform_token::*; 13 | pub use platform_token_scope::*; 14 | 15 | mod company; 16 | mod error_response; 17 | mod platform_token; 18 | mod platform_token_scope; 19 | -------------------------------------------------------------------------------- /common/src/requests.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | use schemars::JsonSchema; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use utoipa::IntoParams; 6 | use utoipa::ToSchema; 7 | 8 | use crate::AccessRequestState; 9 | use crate::Request; 10 | 11 | /// Query parameters for the GET /v1/requests endpoint 12 | #[derive(Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)] 13 | pub struct RequestsGetQueryParams { 14 | /// State of the requests 15 | pub state: AccessRequestState, 16 | /// Number of requests to return 17 | pub count: i64, 18 | } 19 | 20 | route_request_response! { 21 | #[Get] Requests() -> Vec 22 | } 23 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common-macros = { path = "../common-macros" } 10 | 11 | serde_json = { workspace = true } 12 | serde = { workspace = true } 13 | display_json = { workspace = true } 14 | chrono = { workspace = true } 15 | derive_more = { workspace = true } 16 | schemars = { workspace = true } 17 | diesel = { workspace = true } 18 | diesel-derive-enum = { workspace = true } 19 | utoipa = { workspace = true } 20 | paste = { workspace = true } 21 | macro_rules_attribute = { workspace = true } -------------------------------------------------------------------------------- /common-platform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common-platform" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common-macros = { path = "../common-macros" } 10 | derive_more = { workspace = true } 11 | diesel = { workspace = true } 12 | diesel-derive-enum = { workspace = true } 13 | display_json = { workspace = true } 14 | macro_rules_attribute = { workspace = true } 15 | paste = { workspace = true } 16 | schemars = { workspace = true } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | utoipa = { workspace = true } 20 | strum = { workspace = true } -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/user_roles.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_user_roles Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Satounki user access roles 7 | --- 8 | 9 | # satounki_user_roles (Resource) 10 | 11 | Satounki user access roles 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `access_roles` (List of String) Access roles 21 | - `email` (String) Email address registered with Satounki 22 | 23 | ### Read-Only 24 | 25 | - `id` (String) Terraform-generated resource ID 26 | - `last_updated` (String) Time of the last modification to this resource 27 | -------------------------------------------------------------------------------- /client/src/configuration.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct Configuration { 9 | pub aws: Option>, 10 | pub gcp: Option, 11 | pub cloudflare: Option>, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct AwsCredentials { 16 | pub aws_access_key_id: String, 17 | pub aws_secret_access_key: String, 18 | } 19 | 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct CfCredentials { 22 | pub account_id: String, 23 | pub access_token: String, 24 | } 25 | -------------------------------------------------------------------------------- /database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "database" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common = { path = "../common" } 10 | common-platform = { path = "../common-platform" } 11 | 12 | chrono = { workspace = true } 13 | color-eyre = { workspace = true } 14 | derive_more = { workspace = true } 15 | diesel = { workspace = true } 16 | diesel-autoincrement-new-struct = "0.1" 17 | diesel-derive-enum = { workspace = true } 18 | display_json = { workspace = true } 19 | memorable-wordlist = "0.1" 20 | serde = { workspace = true } 21 | serde_json = { workspace = true } 22 | uuid = { version = "1", features = ["v4"] } -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | aws = { path = "../aws" } 10 | chrono = { workspace = true } 11 | clap = { workspace = true } 12 | cloudflare = { path = "../cloudflare" } 13 | color-eyre = { workspace = true } 14 | common = { path = "../common" } 15 | env_logger = { workspace = true } 16 | futures-util = "0.3" 17 | gcloud = { path = "../gcloud" } 18 | log = "0.4" 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | serde_yaml = { workspace = true } 22 | tokio = { workspace = true } 23 | tokio-tungstenite = "0.20" 24 | url = "2" 25 | derive_more = { workspace = true } -------------------------------------------------------------------------------- /terraform-providers/satounki/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. 4 | 5 | The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. 6 | 7 | * **provider/provider.tf** example file for the provider index page 8 | * **data-sources/`full data source name`/data-source.tf** example file for the named data source page 9 | * **resources/`full resource name`/resource.tf** example file for the named data source page 10 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. 4 | 5 | The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. 6 | 7 | * **provider/provider.tf** example file for the provider index page 8 | * **data-sources/`full data source name`/data-source.tf** example file for the named data source page 9 | * **resources/`full resource name`/resource.tf** example file for the named data source page 10 | -------------------------------------------------------------------------------- /migrations/2022-06-23-204541_create_access_requests/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE access_requests 4 | ( 5 | id TEXT PRIMARY KEY NOT NULL, 6 | company_id INTEGER NOT NULL, 7 | requester TEXT NOT NULL REFERENCES users (email), 8 | timestamp TEXT NOT NULL, 9 | duration INTEGER NOT NULL, 10 | approved INTEGER NOT NULL DEFAULT (FALSE), 11 | access_expiry TEXT, 12 | state TEXT NOT NULL, 13 | modified TEXT NOT NULL, 14 | req_alias TEXT UNIQUE NOT NULL, 15 | policy TEXT NOT NULL, 16 | 17 | FOREIGN KEY (company_id, policy) REFERENCES company_policies (company_id, name) ON DELETE NO ACTION 18 | ) -------------------------------------------------------------------------------- /api/src/auth/access_role.rs: -------------------------------------------------------------------------------- 1 | pub use Administrator as AdministratorRole; 2 | pub use Approver as ApproverRole; 3 | pub use User as UserRole; 4 | 5 | pub trait AccessRole { 6 | fn as_enum() -> common::AccessRole; 7 | fn as_str() -> &'static str; 8 | } 9 | 10 | macro_rules! role { 11 | ( $( $name:ident ),+ $(,)? ) => { 12 | $( 13 | paste::paste! { 14 | pub struct $name; 15 | impl AccessRole for $name { 16 | fn as_enum() -> common::AccessRole { 17 | common::AccessRole::$name 18 | } 19 | 20 | fn as_str() -> &'static str { 21 | stringify!([< $name:snake >]) 22 | } 23 | } 24 | } 25 | )+ 26 | }; 27 | } 28 | 29 | role! { 30 | User, 31 | Approver, 32 | Administrator 33 | } 34 | -------------------------------------------------------------------------------- /api/src/auth/platform_token_scope.rs: -------------------------------------------------------------------------------- 1 | pub use Read as ReadScope; 2 | pub use Write as WriteScope; 3 | 4 | pub trait PlatformTokenScope { 5 | fn as_enum() -> common_platform::PlatformTokenScope; 6 | fn as_str() -> &'static str; 7 | } 8 | 9 | macro_rules! scope { 10 | ( $( $name:ident ),+ $(,)? ) => { 11 | $( 12 | paste::paste! { 13 | pub struct $name; 14 | impl PlatformTokenScope for $name { 15 | fn as_enum() -> common_platform::PlatformTokenScope { 16 | common_platform::PlatformTokenScope::$name 17 | } 18 | 19 | fn as_str() -> &'static str { 20 | stringify!([< $name:snake >]) 21 | } 22 | } 23 | } 24 | )+ 25 | }; 26 | } 27 | 28 | scope! { 29 | Read, 30 | Write, 31 | } 32 | -------------------------------------------------------------------------------- /common/src/user_roles.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | use diesel_derive_enum::DbEnum; 3 | use schemars::JsonSchema; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use utoipa::ToSchema; 7 | 8 | use crate::Terraform; 9 | 10 | /// Satounki user access roles 11 | #[apply(Terraform!)] 12 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema, DbEnum)] 13 | #[serde(rename_all = "snake_case")] 14 | pub enum AccessRole { 15 | /// View and make access requests 16 | User, 17 | /// Approve access requests 18 | Approver, 19 | /// Change user roles, grant administrator approval to access requests 20 | Administrator, 21 | } 22 | 23 | route_request_response! { 24 | #[Post] UserRoles(Vec) -> Vec, 25 | #[Put] UserRoles(Vec) -> Vec, 26 | #[Get] UserRoles() -> Vec, 27 | } 28 | -------------------------------------------------------------------------------- /dev_data.sql: -------------------------------------------------------------------------------- 1 | REPLACE INTO users (id, email, first_name, last_name, active) 2 | VALUES (1, 'lgug2z@satounki.com', 'Jeezy', 'LGUG2Z', 1); 3 | 4 | REPLACE INTO users (id, email, first_name, last_name, active) 5 | VALUES (2, 'samir@satounki.com', 'Samir', 'Jan', 1); 6 | 7 | REPLACE INTO companies (id, name, domain, root_user) 8 | VALUES (1, 'Satounki Development', 'satounki.com', 'lgug2z@satounki.com'); 9 | 10 | REPLACE INTO users_companies (user_id, company_id) 11 | VALUES (1, 1); 12 | 13 | REPLACE INTO users_companies (user_id, company_id) 14 | VALUES (2, 1); 15 | 16 | REPLACE INTO worker_keys (company_id, key) 17 | VALUES (1, 'swk-e0c43bd0-38a4-4e7b-9c0f-8bd5f47f20d2'); 18 | 19 | REPLACE INTO api_tokens (token, company_id) 20 | VALUES ('e-mo-tion', 1); 21 | 22 | REPLACE INTO user_tokens (token, user_id) 23 | VALUES ('crj', 1); 24 | 25 | REPLACE INTO user_tokens (token, user_id) 26 | VALUES ('smr', 2) 27 | -------------------------------------------------------------------------------- /common-gen/src/golang/delete.rs: -------------------------------------------------------------------------------- 1 | pub const DELETE_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Delete(id string) error { 3 | url := fmt.Sprintf("%s{{ delete }}", api.BaseURL, id) 4 | req, err := http.NewRequest(http.MethodDelete, url, nil) 5 | if err != nil { 6 | return err 7 | } 8 | 9 | resp, err := api.httpClient.Do(req) 10 | if err != nil { 11 | return err 12 | } 13 | 14 | defer func(Body io.ReadCloser) { 15 | err := Body.Close() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | }(resp.Body) 20 | 21 | respBody, err := io.ReadAll(resp.Body) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 27 | return nil 28 | } else { 29 | response, err := UnmarshalErrorResponse(respBody) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return errors.New(response.Error) 35 | } 36 | } 37 | "#; 38 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_roles_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Satounki user access roles 8 | type userRolesResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Satounki user access roles"` 11 | // Terraform-generated resource ID 12 | ID types.String `tfsdk:"id" rustdoc:"Terraform-generated resource ID"` 13 | // Email address registered with Satounki 14 | Email types.String `tfsdk:"email" rustdoc:"Email address registered with Satounki" resourcedoc:"Satounki user access roles"` 15 | // Access roles 16 | AccessRoles []types.String `tfsdk:"access_roles" rustdoc:"Access roles" resourcedoc:"Satounki user access roles"` 17 | } 18 | -------------------------------------------------------------------------------- /common/src/user_aliases.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | use crate::Schema; 4 | use crate::Terraform; 5 | 6 | /// Service-specific username aliases 7 | #[apply(Terraform!)] 8 | #[apply(Schema!)] 9 | #[serde(rename_all = "snake_case")] 10 | pub struct UserAliases { 11 | /// Username on Amazon Web Services, may not be an email address 12 | #[schema(example = "Samir")] 13 | pub aws: Option, 14 | /// Email address registered with Cloudflare 15 | #[schema(example = "samir@cool-company.com")] 16 | pub cloudflare: Option, 17 | /// Email address registered with Google Cloud Platform 18 | #[schema(example = "samir@cool-company.com")] 19 | pub gcp: Option, 20 | } 21 | 22 | route_request_response! { 23 | #[Post] UserAliases(UserAliases) -> UserAliases, 24 | #[Put] UserAliases(UserAliases) -> UserAliases, 25 | #[Get] UserAliases() -> UserAliases, 26 | } 27 | -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/user_aliases.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_user_aliases Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Service-specific username aliases 7 | --- 8 | 9 | # satounki_user_aliases (Resource) 10 | 11 | Service-specific username aliases 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `email` (String) Email address registered with Satounki 21 | 22 | ### Optional 23 | 24 | - `aws` (String) Username on Amazon Web Services, may not be an email address 25 | - `cloudflare` (String) Email address registered with Cloudflare 26 | - `gcp` (String) Email address registered with Google Cloud Platform 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) Terraform-generated resource ID 31 | - `last_updated` (String) Time of the last modification to this resource 32 | -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/aws_account.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_aws_account Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Amazon Web Services account configuration 7 | --- 8 | 9 | # satounki_aws_account (Resource) 10 | 11 | Amazon Web Services account configuration 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `account` (String) Meaningful alias for the account to be used by Satounki users 21 | - `admin_approval_required` (Boolean) Require additional approval by an Administrator for access requests made to the account 22 | - `approvals_required` (Number) Number of approvals required for access requests made to the account 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) UUID generated by Satounki 27 | - `last_updated` (String) Time of the last modification to this resource 28 | -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/cloudflare_account.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_cloudflare_account Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Cloudflare account configuration 7 | --- 8 | 9 | # satounki_cloudflare_account (Resource) 10 | 11 | Cloudflare account configuration 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `account` (String) Meaningful alias for the account to be used by Satounki users 21 | - `admin_approval_required` (Boolean) Require additional approval by an Administrator for access requests made to the account 22 | - `approvals_required` (Number) Number of approvals required for access requests made to the account 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) UUID generated by Satounki 27 | - `last_updated` (String) Time of the last modification to this resource 28 | -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/gcp_project.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_gcp_project Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Google Cloud Platform project configuration 7 | --- 8 | 9 | # satounki_gcp_project (Resource) 10 | 11 | Google Cloud Platform project configuration 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `admin_approval_required` (Boolean) Require additional approval by an Administrator for access requests made to the project 21 | - `approvals_required` (Number) Number of approvals required for access requests made to the project 22 | - `project` (String) Meaningful alias for the project to be used by Satounki users 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) UUID generated by Satounki 27 | - `last_updated` (String) Time of the last modification to this resource 28 | -------------------------------------------------------------------------------- /common-gen/src/golang/patch_id.rs: -------------------------------------------------------------------------------- 1 | pub const PATCH_ID_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Patch(id string) error { 3 | url := fmt.Sprintf("%s{{ patch }}", api.BaseURL, id) 4 | 5 | req, err := http.NewRequest(http.MethodPatch, 6 | url, 7 | nil, 8 | ) 9 | 10 | req.Header.Add("Content-Type", "application/json") 11 | resp, err := api.httpClient.Do(req) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | defer func(Body io.ReadCloser) { 17 | err := Body.Close() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | }(resp.Body) 22 | 23 | respBody, err := io.ReadAll(resp.Body) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 29 | return nil 30 | } else { 31 | response, err := UnmarshalErrorResponse(respBody) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return errors.New(response.Error) 37 | } 38 | } 39 | "#; 40 | -------------------------------------------------------------------------------- /database/src/company_role.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use common::AccessRole; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::company_roles; 9 | use crate::Result; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 12 | #[diesel(table_name = company_roles)] 13 | pub struct CompanyRole { 14 | pub id: i32, 15 | pub name: AccessRole, 16 | } 17 | 18 | impl CompanyRole { 19 | pub fn read(connection: &mut SqliteConnection, id: i32) -> Result { 20 | company_roles::table 21 | .filter(company_roles::dsl::id.eq(id)) 22 | .first(connection) 23 | } 24 | 25 | pub fn read_by_name(connection: &mut SqliteConnection, role: &AccessRole) -> Result { 26 | company_roles::table 27 | .filter(company_roles::dsl::name.eq(role)) 28 | .first(connection) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/src/justification.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::justifications; 8 | use crate::Result; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 11 | #[diesel(table_name = justifications)] 12 | pub struct Justification { 13 | pub access_request_id: String, 14 | pub justification: String, 15 | } 16 | 17 | impl Justification { 18 | pub fn create(connection: &mut SqliteConnection, justification: Self) -> Result { 19 | diesel::insert_into(justifications::table) 20 | .values(justification) 21 | .get_result(connection) 22 | } 23 | 24 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 25 | use justifications::dsl::justifications; 26 | justifications.find(access_request_id).first(connection) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/src/rejection.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use chrono::DateTime; 4 | use chrono::Utc; 5 | use diesel::prelude::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::schema::rejections; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 13 | #[diesel(table_name = rejections)] 14 | pub struct Rejection { 15 | pub access_request_id: String, 16 | pub user: String, 17 | pub timestamp: DateTime, 18 | } 19 | 20 | impl Rejection { 21 | pub fn create(connection: &mut SqliteConnection, rejection: Self) -> Result { 22 | diesel::insert_into(rejections::table) 23 | .values(rejection) 24 | .get_result(connection) 25 | } 26 | 27 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 28 | use rejections::dsl::rejections; 29 | rejections.find(access_request_id).first(connection) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/2022-09-02-030705_slack/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE company_slack 4 | ( 5 | company_id INTEGER NOT NULL, 6 | team_id TEXT NOT NULL UNIQUE, 7 | team_name TEXT NOT NULL, 8 | channel_id TEXT NOT NULL UNIQUE, 9 | access_token TEXT NOT NULL UNIQUE, 10 | incoming_webhook TEXT NOT NULL UNIQUE, 11 | 12 | FOREIGN KEY (company_id) 13 | REFERENCES companies (id) ON DELETE NO ACTION, 14 | 15 | PRIMARY KEY (company_id) 16 | ); 17 | 18 | 19 | CREATE TABLE requests_slack 20 | ( 21 | access_request_id TEXT NOT NULL, 22 | company_id INTEGER NOT NULL, 23 | channel_id TEXT NOT NULL, 24 | ts TEXT NOT NULL UNIQUE, 25 | 26 | FOREIGN KEY (access_request_id) 27 | REFERENCES access_requests (id) ON DELETE CASCADE, 28 | FOREIGN KEY (company_id) 29 | REFERENCES companies (id) ON DELETE NO ACTION, 30 | 31 | PRIMARY KEY (access_request_id) 32 | ); -------------------------------------------------------------------------------- /database/src/request_slack.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::requests_slack; 8 | use crate::Result; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 11 | #[diesel(table_name = requests_slack)] 12 | pub struct RequestSlack { 13 | pub access_request_id: String, 14 | pub company_id: i32, 15 | pub channel_id: String, 16 | pub ts: String, 17 | } 18 | 19 | impl RequestSlack { 20 | pub fn create(connection: &mut SqliteConnection, request: Self) -> Result { 21 | diesel::insert_into(requests_slack::table) 22 | .values(request) 23 | .get_result(connection) 24 | } 25 | 26 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 27 | use requests_slack::dsl::requests_slack; 28 | requests_slack.find(access_request_id).first(connection) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/docs/resources/company.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounkiplatform_company Resource - satounkiplatform" 4 | subcategory: "" 5 | description: |- 6 | Company 7 | --- 8 | 9 | # satounkiplatform_company (Resource) 10 | 11 | Company 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `domain` (String) Email domain of the company (G-Suite etc.) 21 | - `name` (String) Name of the company 22 | - `root_user_email` (String) Company root user's email address 23 | - `root_user_first_name` (String) Company root user's first name 24 | - `root_user_last_name` (String) Company root user's last name 25 | 26 | ### Read-Only 27 | 28 | - `api_token` (String, Sensitive) Company API token 29 | - `id` (String) Auto-incrementing integer 30 | - `last_updated` (String) Time of the last modification to this resource 31 | - `worker_key` (String, Sensitive) Company worker key 32 | -------------------------------------------------------------------------------- /api/src/auth/individual_user.rs: -------------------------------------------------------------------------------- 1 | use actix::fut::ready; 2 | use actix::fut::Ready; 3 | use actix_web::dev::Payload; 4 | use actix_web::FromRequest; 5 | use actix_web::HttpRequest; 6 | use derive_more::Deref; 7 | 8 | use crate::auth::authenticated::Authenticated; 9 | use crate::error; 10 | 11 | #[derive(Deref)] 12 | pub struct IndividualUser(pub Authenticated); 13 | 14 | impl FromRequest for IndividualUser { 15 | type Error = actix_web::Error; 16 | type Future = Ready>; 17 | 18 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 19 | if let Ok(auth) = Authenticated::from_request(req, payload).into_inner() { 20 | if matches!( 21 | auth, 22 | Authenticated::Cookie { .. } | Authenticated::UserToken { .. } 23 | ) { 24 | return ready(Ok(Self(auth))); 25 | } 26 | } 27 | 28 | ready(Err(error::Api::UnauthorizedUserCredentialsRequired.into())) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /terraform-providers/satounki/docs/resources/policy.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "satounki_policy Resource - satounki" 4 | subcategory: "" 5 | description: |- 6 | Satounki Policy definition 7 | --- 8 | 9 | # satounki_policy (Resource) 10 | 11 | Satounki Policy definition 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `description` (String) Description of the permissions granted by this policy 21 | - `name` (String) Succinct, descriptive name for the policy in snake_case 22 | 23 | ### Optional 24 | 25 | - `aws` (List of String) Amazon Web Services policy ARNs associated with this policy 26 | - `cloudflare` (List of String) Cloudflare roles associated with this policy 27 | - `gcp` (List of String) Google Cloud Platform roles associated with this policy 28 | 29 | ### Read-Only 30 | 31 | - `id` (String) UUID generated by Satounki 32 | - `last_updated` (String) Time of the last modification to this resource 33 | -------------------------------------------------------------------------------- /database/src/approval.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use chrono::DateTime; 4 | use chrono::Utc; 5 | use diesel::prelude::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::schema::approvals; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 13 | #[diesel(table_name = approvals)] 14 | pub struct Approval { 15 | pub access_request_id: String, 16 | pub user: String, 17 | pub timestamp: DateTime, 18 | } 19 | 20 | impl Approval { 21 | pub fn create(connection: &mut SqliteConnection, approval: Self) -> Result { 22 | diesel::insert_into(approvals::table) 23 | .values(approval) 24 | .get_result(connection) 25 | } 26 | 27 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result> { 28 | approvals::table 29 | .filter(approvals::dsl::access_request_id.eq(access_request_id)) 30 | .load::<_>(connection) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/src/revocation.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use chrono::DateTime; 4 | use chrono::Utc; 5 | use diesel::prelude::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::schema::revocations; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 13 | #[diesel(table_name = revocations)] 14 | pub struct Revocation { 15 | pub access_request_id: String, 16 | pub user: String, 17 | pub timestamp: DateTime, 18 | } 19 | 20 | impl Revocation { 21 | pub fn create(connection: &mut SqliteConnection, revocation: Self) -> Result { 22 | diesel::insert_into(revocations::table) 23 | .values(revocation) 24 | .on_conflict_do_nothing() 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 29 | use revocations::dsl::revocations; 30 | revocations.find(access_request_id).first(connection) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/src/access_request_state.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use diesel_derive_enum::DbEnum; 3 | use schemars::JsonSchema; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use utoipa::ToSchema; 7 | 8 | /// State in the access request lifecycle 9 | #[derive(Debug, Display, Copy, Clone, Serialize, Deserialize, DbEnum, JsonSchema, ToSchema)] 10 | #[schema(example = "active")] 11 | #[serde(rename_all = "snake_case")] 12 | pub enum AccessRequestState { 13 | /// Request has been submitted and may or may not have met required approvals 14 | Pending, 15 | /// Request has been approved and the permissions associated with the policy have been granted 16 | Active, 17 | /// Request has expired or been marked as completed early by the requesting user 18 | Completed, 19 | /// Request has been cancelled before approval by the requesting user 20 | Cancelled, 21 | /// Request has been rejected by an Approver or an Administrator 22 | Rejected, 23 | /// Request was active, but revoked by an Administrator 24 | Revoked, 25 | } 26 | -------------------------------------------------------------------------------- /database/src/extension.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use chrono::DateTime; 4 | use chrono::Utc; 5 | use diesel::prelude::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::schema::extensions; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 13 | #[diesel(table_name = extensions)] 14 | pub struct Extension { 15 | pub access_request_id: String, 16 | pub user: String, 17 | pub timestamp: DateTime, 18 | pub duration: i32, 19 | } 20 | 21 | impl Extension { 22 | pub fn create(connection: &mut SqliteConnection, extension: Self) -> Result { 23 | diesel::insert_into(extensions::table) 24 | .values(extension) 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result> { 29 | extensions::table 30 | .filter(extensions::dsl::access_request_id.eq(access_request_id)) 31 | .load::<_>(connection) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/src/cancellation.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use chrono::DateTime; 4 | use chrono::Utc; 5 | use diesel::prelude::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::schema::cancellations; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 13 | #[diesel(table_name = cancellations)] 14 | pub struct Cancellation { 15 | pub access_request_id: String, 16 | pub user: String, 17 | pub timestamp: DateTime, 18 | } 19 | 20 | impl Cancellation { 21 | pub fn create(connection: &mut SqliteConnection, cancellation: Self) -> Result { 22 | diesel::insert_into(cancellations::table) 23 | .values(cancellation) 24 | .on_conflict_do_nothing() 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 29 | use cancellations::dsl::cancellations; 30 | cancellations.find(access_request_id).first(connection) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common-platform/src/company.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | use crate::New; 4 | use crate::Schema; 5 | use crate::Terraform; 6 | 7 | /// Company 8 | #[apply(New!)] 9 | #[apply(Terraform!)] 10 | #[apply(Schema!)] 11 | pub struct Company { 12 | /// Auto-incrementing integer 13 | pub id: i32, 14 | /// Name of the company 15 | pub name: String, 16 | /// Email domain of the company (G-Suite etc.) 17 | pub domain: String, 18 | /// Company root user's email address 19 | pub root_user_email: String, 20 | /// Company root user's first name 21 | pub root_user_first_name: Option, 22 | /// Company root user's last name 23 | pub root_user_last_name: Option, 24 | /// Company API token 25 | pub api_token: Option, 26 | /// Company worker key 27 | pub worker_key: Option, 28 | } 29 | 30 | route_request_response! { 31 | #[Put] Company(Company) -> Company, 32 | #[Post] Company(Company) -> Company, 33 | #[Get] Company() -> Company, 34 | #[Get] Companies() -> Vec, 35 | } 36 | -------------------------------------------------------------------------------- /common-gen/src/golang/patch_id_body.rs: -------------------------------------------------------------------------------- 1 | pub const PATCH_ID_BODY_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Patch(id string, body {{ name }}PatchRequest) error { 3 | url := fmt.Sprintf("%s{{ patch }}", api.BaseURL, id) 4 | reqBody, err := json.Marshal(&body) 5 | if err != nil { 6 | return err 7 | } 8 | 9 | req, err := http.NewRequest(http.MethodPatch, 10 | url, 11 | bytes.NewBuffer(reqBody), 12 | ) 13 | 14 | req.Header.Add("Content-Type", "application/json") 15 | resp, err := api.httpClient.Do(req) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | defer func(Body io.ReadCloser) { 21 | err := Body.Close() 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | }(resp.Body) 26 | 27 | respBody, err := io.ReadAll(resp.Body) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 33 | return nil 34 | } else { 35 | response, err := UnmarshalErrorResponse(respBody) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return errors.New(response.Error) 41 | } 42 | } 43 | "#; 44 | -------------------------------------------------------------------------------- /common-gen/src/golang/get_list.rs: -------------------------------------------------------------------------------- 1 | pub const GET_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Get() ({{ name }}GetResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ get }}", api.BaseURL) 4 | 5 | resp, err := api.httpClient.Get(url) 6 | if err != nil { 7 | return {{ name }}GetResponse{}, nil, err 8 | } 9 | 10 | defer func(Body io.ReadCloser) { 11 | err := Body.Close() 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | }(resp.Body) 16 | 17 | respBody, err := io.ReadAll(resp.Body) 18 | if err != nil { 19 | return {{ name }}GetResponse{}, nil, err 20 | } 21 | 22 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 23 | response, err := Unmarshal{{ name }}GetResponse(respBody) 24 | if err != nil { 25 | return {{ name }}GetResponse{}, nil, err 26 | } 27 | 28 | return response, nil, nil 29 | } else { 30 | response, err := UnmarshalErrorResponse(respBody) 31 | if err != nil { 32 | return {{ name }}GetResponse{}, nil, err 33 | } 34 | 35 | return {{ name }}GetResponse{}, &response, errors.New(response.Error) 36 | } 37 | } 38 | "#; 39 | -------------------------------------------------------------------------------- /satounki-go/satounki.go: -------------------------------------------------------------------------------- 1 | package satounki 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type API struct { 9 | APIToken string 10 | BaseURL string 11 | UserAgent string 12 | httpClient *http.Client 13 | } 14 | 15 | func New(token, baseURL, userAgent string, client *http.Client) *API { 16 | api := &API{ 17 | APIToken: token, 18 | BaseURL: baseURL, 19 | UserAgent: userAgent, 20 | httpClient: client, 21 | } 22 | 23 | // Fall back to http.DefaultClient if the package user does not provide 24 | // their own. 25 | if api.httpClient == nil { 26 | api.httpClient = http.DefaultClient 27 | } 28 | 29 | api.httpClient.Transport = &customTransport{ 30 | token: token, 31 | userAgent: userAgent, 32 | } 33 | 34 | return api 35 | } 36 | 37 | type customTransport struct { 38 | token string 39 | userAgent string 40 | } 41 | 42 | func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { 43 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", t.token)) 44 | req.Header.Add("User-Agent", t.userAgent) 45 | return http.DefaultTransport.RoundTrip(req) 46 | } 47 | -------------------------------------------------------------------------------- /satounki-platform-go/satounki.go: -------------------------------------------------------------------------------- 1 | package satounki 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type API struct { 9 | APIToken string 10 | BaseURL string 11 | UserAgent string 12 | httpClient *http.Client 13 | } 14 | 15 | func New(token, baseURL, userAgent string, client *http.Client) *API { 16 | api := &API{ 17 | APIToken: token, 18 | BaseURL: baseURL, 19 | UserAgent: userAgent, 20 | httpClient: client, 21 | } 22 | 23 | // Fall back to http.DefaultClient if the package user does not provide 24 | // their own. 25 | if api.httpClient == nil { 26 | api.httpClient = http.DefaultClient 27 | } 28 | 29 | api.httpClient.Transport = &customTransport{ 30 | token: token, 31 | userAgent: userAgent, 32 | } 33 | 34 | return api 35 | } 36 | 37 | type customTransport struct { 38 | token string 39 | userAgent string 40 | } 41 | 42 | func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { 43 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", t.token)) 44 | req.Header.Add("User-Agent", t.userAgent) 45 | return http.DefaultTransport.RoundTrip(req) 46 | } 47 | -------------------------------------------------------------------------------- /common-gen/src/golang/get_id.rs: -------------------------------------------------------------------------------- 1 | pub const GET_ID_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Get(id string) ({{ name }}GetResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ get }}", api.BaseURL, id) 4 | 5 | resp, err := api.httpClient.Get(url) 6 | if err != nil { 7 | return {{ name }}GetResponse{}, nil, err 8 | } 9 | 10 | defer func(Body io.ReadCloser) { 11 | err := Body.Close() 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | }(resp.Body) 16 | 17 | respBody, err := io.ReadAll(resp.Body) 18 | if err != nil { 19 | return {{ name }}GetResponse{}, nil, err 20 | } 21 | 22 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 23 | response, err := Unmarshal{{ name }}GetResponse(respBody) 24 | if err != nil { 25 | return {{ name }}GetResponse{}, nil, err 26 | } 27 | 28 | return response, nil, nil 29 | } else { 30 | response, err := UnmarshalErrorResponse(respBody) 31 | if err != nil { 32 | return {{ name }}GetResponse{}, nil, err 33 | } 34 | 35 | return {{ name }}GetResponse{}, &response, errors.New(response.Error) 36 | } 37 | } 38 | "#; 39 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate macro_rules_attribute; 3 | 4 | pub use access_request_state::*; 5 | pub use error_response::*; 6 | pub use permissions_action::*; 7 | pub use policy::*; 8 | pub use request_alias::*; 9 | pub use request_policy::*; 10 | pub use requests::*; 11 | pub use roles::*; 12 | pub use settings::*; 13 | pub use user_aliases::*; 14 | pub use user_roles::*; 15 | pub use user_status::*; 16 | pub use user_token::*; 17 | pub use websocket::*; 18 | 19 | attribute_alias! { 20 | #[apply(Schema!)] = #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, display_json::DisplayAsJsonPretty, schemars::JsonSchema, utoipa::ToSchema)]; 21 | #[apply(Terraform!)] = #[macro_rules_derive(common_macros::terraform_resource!)]; 22 | #[apply(New!)] = #[macro_rules_derive(common_macros::new_resource!)]; 23 | } 24 | 25 | mod access_request_state; 26 | mod error_response; 27 | mod permissions_action; 28 | mod policy; 29 | mod request_alias; 30 | mod request_policy; 31 | mod requests; 32 | mod roles; 33 | mod settings; 34 | mod user_aliases; 35 | mod user_roles; 36 | mod user_status; 37 | mod user_token; 38 | mod websocket; 39 | -------------------------------------------------------------------------------- /database/src/user_company.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::users_companies; 8 | use crate::Result; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 11 | #[diesel(table_name = users_companies)] 12 | pub struct UserCompany { 13 | pub user_id: i32, 14 | pub company_id: i32, 15 | } 16 | 17 | impl UserCompany { 18 | pub fn create(connection: &mut SqliteConnection, user_company: Self) -> Result { 19 | diesel::insert_into(users_companies::table) 20 | .values(user_company) 21 | .get_result(connection) 22 | } 23 | 24 | pub fn delete( 25 | connection: &mut SqliteConnection, 26 | user_id: i32, 27 | company_id: i32, 28 | ) -> Result { 29 | use users_companies::dsl; 30 | 31 | diesel::delete( 32 | dsl::users_companies 33 | .filter(dsl::user_id.eq(user_id)) 34 | .filter(dsl::company_id.eq(company_id)), 35 | ) 36 | .execute(connection) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api/src/auth/api_token.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use actix::fut::ready; 4 | use actix::fut::Ready; 5 | use actix_web::dev::Payload; 6 | use actix_web::FromRequest; 7 | use actix_web::HttpRequest; 8 | 9 | use crate::auth::authenticated::Authenticated; 10 | use crate::error; 11 | 12 | #[derive(Clone)] 13 | pub struct ApiToken(pub Authenticated); 14 | 15 | impl Deref for ApiToken { 16 | type Target = Authenticated; 17 | 18 | fn deref(&self) -> &Self::Target { 19 | &self.0 20 | } 21 | } 22 | 23 | impl FromRequest for ApiToken { 24 | type Error = actix_web::Error; 25 | type Future = Ready>; 26 | 27 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 28 | match Authenticated::from_request(req, payload).into_inner() { 29 | Ok(auth) => { 30 | if matches!(auth, Authenticated::ApiToken { .. }) { 31 | ready(Ok(Self(auth))) 32 | } else { 33 | ready(Err(error::Api::UnauthorizedApiTokenRequired.into())) 34 | } 35 | } 36 | Err(error) => ready(Err(error)), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common-gen/src/typescript/mod.rs: -------------------------------------------------------------------------------- 1 | pub use delete::*; 2 | pub use get_id::*; 3 | pub use get_list::*; 4 | pub use patch_id::*; 5 | pub use patch_id_body::*; 6 | pub use post::*; 7 | pub use post_id::*; 8 | pub use put::*; 9 | pub use put_id::*; 10 | pub use put_id_body::*; 11 | 12 | mod delete; 13 | mod get_id; 14 | mod get_list; 15 | mod patch_id; 16 | mod patch_id_body; 17 | mod post; 18 | mod post_id; 19 | mod put; 20 | mod put_id; 21 | mod put_id_body; 22 | 23 | pub const TS_TEMPLATE: &str = r#"{{ comment }} 24 | 25 | import { IRestResponse, RestClient } from "typed-rest-client/RestClient"; 26 | import * as satounki from "./types.generated"; 27 | 28 | export class Api { 29 | private apiToken: string; 30 | private baseUrl: string; 31 | private userAgent: string; 32 | client: RestClient; 33 | 34 | constructor(apiToken: string, baseUrl: string, userAgent: string) { 35 | let restClient = new RestClient(userAgent, baseUrl, [], { 36 | headers: { 37 | "Authorization": `Bearer ${apiToken}`, 38 | }, 39 | }); 40 | 41 | this.apiToken = apiToken; 42 | this.baseUrl = baseUrl; 43 | this.userAgent = userAgent; 44 | this.client = restClient; 45 | } 46 | 47 | {{ generated }} 48 | } 49 | "#; 50 | -------------------------------------------------------------------------------- /common-gen/src/golang/put.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Put() ({{ name }}PutResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ put }}", api.BaseURL) 4 | 5 | req, err := http.NewRequest(http.MethodPut, url, nil) 6 | if err != nil { 7 | return {{ name }}PutResponse{}, nil, err 8 | } 9 | 10 | req.Header.Add("Content-Type", "application/json") 11 | resp, err := api.httpClient.Do(req) 12 | if err != nil { 13 | return {{ name }}PutResponse{}, nil, err 14 | } 15 | 16 | defer func(Body io.ReadCloser) { 17 | err := Body.Close() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | }(resp.Body) 22 | 23 | respBody, err := io.ReadAll(resp.Body) 24 | if err != nil { 25 | return {{ name }}PutResponse{}, nil, err 26 | } 27 | 28 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 29 | response, err := Unmarshal{{ name }}PutResponse(respBody) 30 | if err != nil { 31 | return {{ name }}PutResponse{}, nil, err 32 | } 33 | 34 | return response, nil, nil 35 | } else { 36 | response, err := UnmarshalErrorResponse(respBody) 37 | if err != nil { 38 | return {{ name }}PutResponse{}, nil, err 39 | } 40 | 41 | return {{ name }}PutResponse{}, &response, errors.New(response.Error) 42 | } 43 | } 44 | "#; 45 | -------------------------------------------------------------------------------- /api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | common = { path = "../common" } 10 | common-platform = { path = "../common-platform" } 11 | database = { path = "../database" } 12 | slack = { path = "../slack" } 13 | 14 | actix = "0.13" 15 | actix-session = { version = "0.8", features = ["cookie-session"] } 16 | actix-web-actors = "4" 17 | actix-web-httpauth = "0.8.0" 18 | paste = { workspace = true } 19 | chrono = { workspace = true } 20 | color-eyre = { workspace = true } 21 | derive_more = { workspace = true } 22 | dotenv = "0.15" 23 | env_logger = { workspace = true } 24 | lazy_static = "1.4" 25 | log = "0.4" 26 | oauth2 = "4.2" 27 | openidconnect = "3" 28 | parking_lot = "0.12" 29 | serde = { workspace = true } 30 | serde_json = { workspace = true } 31 | strum = { workspace = true } 32 | tera = "1.17" 33 | thiserror = { workspace = true } 34 | utoipa = { workspace = true } 35 | utoipa-swagger-ui = { workspace = true } 36 | uuid = { version = "1", features = ["serde", "v4"] } 37 | slack-blocks = "0.25" 38 | reqwest = { workspace = true } 39 | diesel_migrations = "2" 40 | 41 | [dependencies.actix-web] 42 | version = "4" 43 | features = [ 44 | "cookies" 45 | ] -------------------------------------------------------------------------------- /common-gen/src/golang/put_id.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_ID_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Put(id string) ({{ name }}PutResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ put }}", api.BaseURL, id) 4 | req, err := http.NewRequest(http.MethodPut, url, nil) 5 | if err != nil { 6 | return {{ name }}PutResponse{}, nil, err 7 | } 8 | 9 | req.Header.Add("Content-Type", "application/json") 10 | resp, err := api.httpClient.Do(req) 11 | if err != nil { 12 | return {{ name }}PutResponse{}, nil, err 13 | } 14 | 15 | defer func(Body io.ReadCloser) { 16 | err := Body.Close() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | }(resp.Body) 21 | 22 | respBody, err := io.ReadAll(resp.Body) 23 | if err != nil { 24 | return {{ name }}PutResponse{}, nil, err 25 | } 26 | 27 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 28 | response, err := Unmarshal{{ name }}PutResponse(respBody) 29 | if err != nil { 30 | return {{ name }}PutResponse{}, nil, err 31 | } 32 | 33 | return response, nil, nil 34 | } else { 35 | response, err := UnmarshalErrorResponse(respBody) 36 | if err != nil { 37 | return {{ name }}PutResponse{}, nil, err 38 | } 39 | 40 | return {{ name }}PutResponse{}, &response, errors.New(response.Error) 41 | } 42 | } 43 | "#; 44 | -------------------------------------------------------------------------------- /common-gen/src/golang/post.rs: -------------------------------------------------------------------------------- 1 | pub const POST_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Post(body {{ name }}PostRequest) ({{ name }}PostResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ post }}", api.BaseURL) 4 | reqBody, err := json.Marshal(&body) 5 | if err != nil { 6 | return {{ name }}PostResponse{}, nil, err 7 | } 8 | 9 | resp, err := api.httpClient.Post( 10 | url, 11 | "application/json", 12 | bytes.NewBuffer(reqBody), 13 | ) 14 | 15 | if err != nil { 16 | return {{ name }}PostResponse{}, nil, err 17 | } 18 | 19 | defer func(Body io.ReadCloser) { 20 | err := Body.Close() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | }(resp.Body) 25 | 26 | respBody, err := io.ReadAll(resp.Body) 27 | if err != nil { 28 | return {{ name }}PostResponse{}, nil, err 29 | } 30 | 31 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 32 | response, err := Unmarshal{{ name }}PostResponse(respBody) 33 | if err != nil { 34 | return {{ name }}PostResponse{}, nil, err 35 | } 36 | 37 | return response, nil, nil 38 | } else { 39 | response, err := UnmarshalErrorResponse(respBody) 40 | if err != nil { 41 | return {{ name }}PostResponse{}, nil, err 42 | } 43 | 44 | return {{ name }}PostResponse{}, &response, errors.New(response.Error) 45 | } 46 | } 47 | "#; 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "api", 6 | "aws", 7 | "cli", 8 | "client", 9 | "cloudflare", 10 | "common", 11 | "common-macros", 12 | "common-platform", 13 | "common-gen", 14 | "database", 15 | "gcloud", 16 | "rolescraper", 17 | "slack", 18 | ] 19 | 20 | [workspace.dependencies] 21 | chrono = { version = "0.4", features = ["serde"] } 22 | clap = { version = "4", features = ["derive", "env"] } 23 | color-eyre = "0.6" 24 | derive_more = "0.99" 25 | diesel = { version = "2", features = [ "r2d2", "sqlite", "chrono", "serde_json", "returning_clauses_for_sqlite_3_35" ] } 26 | diesel-derive-enum = { version = "2", features = [ "sqlite" ] } 27 | display_json = "0.2" 28 | env_logger = "0.10" 29 | macro_rules_attribute = "0.2" 30 | paste = "1" 31 | reqwest = { version = "0.11", features = ["blocking", "rustls", "json"] } 32 | schemars = { version = "0.8", features = ["derive", "chrono", "preserve_order"] } 33 | serde = { version = "1", features = ["derive"] } 34 | serde_json = "1" 35 | serde_yaml = "0.8" 36 | strum = { version = "0.25", features = ["derive"] } 37 | thiserror = "1" 38 | tokio = { version = "1", features = ["full"] } 39 | utoipa = { version = "4", features = ["actix_extras", "debug", "chrono", "uuid"] } 40 | utoipa-swagger-ui = { version = "4", features = ["actix-web"] } 41 | -------------------------------------------------------------------------------- /common-gen/src/golang/post_id.rs: -------------------------------------------------------------------------------- 1 | pub const POST_ID_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Post(id string, body {{ name }}PostRequest) ({{ name }}PostResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ post }}", api.BaseURL, id) 4 | reqBody, err := json.Marshal(&body) 5 | if err != nil { 6 | return {{ name }}PostResponse{}, nil, err 7 | } 8 | 9 | resp, err := api.httpClient.Post( 10 | url, 11 | "application/json", 12 | bytes.NewBuffer(reqBody), 13 | ) 14 | 15 | if err != nil { 16 | return {{ name }}PostResponse{}, nil, err 17 | } 18 | 19 | defer func(Body io.ReadCloser) { 20 | err := Body.Close() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | }(resp.Body) 25 | 26 | respBody, err := io.ReadAll(resp.Body) 27 | if err != nil { 28 | return {{ name }}PostResponse{}, nil, err 29 | } 30 | 31 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 32 | response, err := Unmarshal{{ name }}PostResponse(respBody) 33 | if err != nil { 34 | return {{ name }}PostResponse{}, nil, err 35 | } 36 | 37 | return response, nil, nil 38 | } else { 39 | response, err := UnmarshalErrorResponse(respBody) 40 | if err != nil { 41 | return {{ name }}PostResponse{}, nil, err 42 | } 43 | 44 | return {{ name }}PostResponse{}, &response, errors.New(response.Error) 45 | } 46 | } 47 | "#; 48 | -------------------------------------------------------------------------------- /database/src/user_token.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | 8 | use crate::schema::user_tokens; 9 | use crate::Result; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable, AsChangeset)] 12 | #[diesel(table_name = user_tokens)] 13 | pub struct UserToken { 14 | pub token: String, 15 | pub user_id: i32, 16 | } 17 | 18 | impl UserToken { 19 | pub fn create(connection: &mut SqliteConnection, user_id: i32) -> Result { 20 | diesel::insert_into(user_tokens::table) 21 | .values(Self { 22 | token: Uuid::new_v4().to_string(), 23 | user_id, 24 | }) 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, token: &str) -> Result { 29 | user_tokens::table.find(token).first(connection) 30 | } 31 | 32 | pub fn delete(connection: &mut SqliteConnection, user_id: i32) -> Result { 33 | use user_tokens::dsl; 34 | 35 | diesel::delete(dsl::user_tokens.filter(dsl::user_id.eq(user_id))).execute(connection) 36 | } 37 | 38 | pub fn replace(connection: &mut SqliteConnection, user_id: i32) -> Result { 39 | Self::delete(connection, user_id)?; 40 | Self::create(connection, user_id) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/cloudflare_account_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Cloudflare account configuration 8 | type cloudflareAccountResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Cloudflare account configuration"` 11 | // UUID generated by Satounki 12 | ID types.String `tfsdk:"id" rustdoc:"UUID generated by Satounki" resourcedoc:"Cloudflare account configuration"` 13 | // Meaningful alias for the account to be used by Satounki users 14 | Account types.String `tfsdk:"account" rustdoc:"Meaningful alias for the account to be used by Satounki users" resourcedoc:"Cloudflare account configuration"` 15 | // Number of approvals required for access requests made to the account 16 | ApprovalsRequired types.Int64 `tfsdk:"approvals_required" rustdoc:"Number of approvals required for access requests made to the account" resourcedoc:"Cloudflare account configuration"` 17 | // Require additional approval by an Administrator for access requests made to the account 18 | AdminApprovalRequired types.Bool `tfsdk:"admin_approval_required" rustdoc:"Require additional approval by an Administrator for access requests made to the account" resourcedoc:"Cloudflare account configuration"` 19 | } 20 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_aliases_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Service-specific username aliases 8 | type userAliasesResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Service-specific username aliases"` 11 | // Terraform-generated resource ID 12 | ID types.String `tfsdk:"id" rustdoc:"Terraform-generated resource ID" resourcedoc:"Service-specific username aliases"` 13 | // Email address registered with Satounki 14 | Email types.String `tfsdk:"email" rustdoc:"Email address registered with Satounki" resourcedoc:"Service-specific username aliases"` 15 | // Username on Amazon Web Services, may not be an email address 16 | Aws types.String `tfsdk:"aws" rustdoc:"Username on Amazon Web Services, may not be an email address" resourcedoc:"Service-specific username aliases"` 17 | // Email address registered with Cloudflare 18 | Cloudflare types.String `tfsdk:"cloudflare" rustdoc:"Email address registered with Cloudflare" resourcedoc:"Service-specific username aliases"` 19 | // Email address registered with Google Cloud Platform 20 | Gcp types.String `tfsdk:"gcp" rustdoc:"Email address registered with Google Cloud Platform" resourcedoc:"Service-specific username aliases"` 21 | } 22 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/aws_account_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Amazon Web Services account configuration 8 | type awsAccountResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Amazon Web Services account configuration"` 11 | // UUID generated by Satounki 12 | ID types.String `tfsdk:"id" rustdoc:"UUID generated by Satounki" resourcedoc:"Amazon Web Services account configuration"` 13 | // Meaningful alias for the account to be used by Satounki users 14 | Account types.String `tfsdk:"account" rustdoc:"Meaningful alias for the account to be used by Satounki users" resourcedoc:"Amazon Web Services account configuration"` 15 | // Number of approvals required for access requests made to the account 16 | ApprovalsRequired types.Int64 `tfsdk:"approvals_required" rustdoc:"Number of approvals required for access requests made to the account" resourcedoc:"Amazon Web Services account configuration"` 17 | // Require additional approval by an Administrator for access requests made to the account 18 | AdminApprovalRequired types.Bool `tfsdk:"admin_approval_required" rustdoc:"Require additional approval by an Administrator for access requests made to the account" resourcedoc:"Amazon Web Services account configuration"` 19 | } 20 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_roles_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | ) 11 | 12 | func (r *userRolesResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 13 | resp.Schema = schema.Schema{ 14 | Description: resourceDoc(userRolesResourceData{}), 15 | Attributes: map[string]schema.Attribute{ 16 | "id": schema.StringAttribute{ 17 | Description: fieldDoc(userRolesResourceData{}, "id"), 18 | Computed: true, 19 | PlanModifiers: []planmodifier.String{ 20 | stringplanmodifier.UseStateForUnknown(), 21 | }, 22 | }, 23 | "last_updated": schema.StringAttribute{ 24 | Description: fieldDoc(userRolesResourceData{}, "last_updated"), 25 | Computed: true, 26 | }, 27 | "email": schema.StringAttribute{ 28 | Description: fieldDoc(userRolesResourceData{}, "email"), 29 | Required: true, 30 | }, 31 | "access_roles": schema.ListAttribute{ 32 | ElementType: types.StringType, 33 | Description: fieldDoc(userRolesResourceData{}, "access_roles"), 34 | Required: true, 35 | }, 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/request_policy.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | use crate::Schema; 4 | 5 | /// Access request for policy permissions 6 | #[apply(Schema!)] 7 | pub struct PolicyRequest { 8 | /// Duration of the request in minutes 9 | #[schema(example = 60)] 10 | pub minutes: i32, 11 | /// Reason for the request 12 | #[schema(example = "to investigate incident #4321 on production")] 13 | pub justification: String, 14 | /// AWS account to grant permissions on, if the policy includes AWS policy ARNs 15 | #[schema(example = "cool-company-production")] 16 | pub aws_account: Option, 17 | /// Cloudflare account to grant permissions on, if the policy includes Cloudflare roles 18 | #[schema(example = "cool-company.com")] 19 | pub cloudflare_account: Option, 20 | /// GCP project to grant permissions on, if the policy includes GCP roles 21 | #[schema(example = "cool-company-production")] 22 | pub gcp_project: Option, 23 | } 24 | 25 | /// Access request confirmation 26 | #[apply(Schema!)] 27 | pub struct PolicyRequestConfirmation { 28 | /// UUID generated by Satounki 29 | #[schema(example = "5da0230c-4eeb-4840-9ffa-d97f45a12182")] 30 | pub request_id: String, 31 | /// Human-friendly alias generated by Satounki 32 | #[schema(example = "samir-crazy-skyline-fish")] 33 | pub request_alias: String, 34 | } 35 | 36 | route_request_response! { 37 | #[Post] RequestPolicy(PolicyRequest) -> PolicyRequestConfirmation 38 | } 39 | -------------------------------------------------------------------------------- /api/templates/policies.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Policies{% endblock title %} 3 | 4 | {% block navigation %} 5 | 8 | 11 | 14 | 17 | 20 | {% endblock navigation %} 21 | 22 | {% block content %} 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for policy in policies %} 33 | 34 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
NameDescription
35 | 36 | {{ policy.name }} 37 | 38 | {{ policy.description }}
44 |
45 | {% endblock content %} -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/gcp_project_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Google Cloud Platform project configuration 8 | type gcpProjectResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Google Cloud Platform project configuration"` 11 | // UUID generated by Satounki 12 | ID types.String `tfsdk:"id" rustdoc:"UUID generated by Satounki" resourcedoc:"Google Cloud Platform project configuration"` 13 | // Meaningful alias for the project to be used by Satounki users 14 | Project types.String `tfsdk:"project" rustdoc:"Meaningful alias for the project to be used by Satounki users" resourcedoc:"Google Cloud Platform project configuration"` 15 | // Number of approvals required for access requests made to the project 16 | ApprovalsRequired types.Int64 `tfsdk:"approvals_required" rustdoc:"Number of approvals required for access requests made to the project" resourcedoc:"Google Cloud Platform project configuration"` 17 | // Require additional approval by an Administrator for access requests made to the project 18 | AdminApprovalRequired types.Bool `tfsdk:"admin_approval_required" rustdoc:"Require additional approval by an Administrator for access requests made to the project" resourcedoc:"Google Cloud Platform project configuration"` 19 | } 20 | -------------------------------------------------------------------------------- /common-gen/src/golang/put_id_body.rs: -------------------------------------------------------------------------------- 1 | pub const PUT_ID_BODY_TEMPLATE_GO: &str = r#" 2 | func (api *API) {{ name }}Put(id string, body {{ name }}PutRequest) ({{ name }}PutResponse, *ErrorResponse, error) { 3 | url := fmt.Sprintf("%s{{ put }}", api.BaseURL, id) 4 | reqBody, err := json.Marshal(&body) 5 | if err != nil { 6 | return {{ name }}PutResponse{}, nil, err 7 | } 8 | 9 | req, err := http.NewRequest(http.MethodPut, 10 | url, 11 | bytes.NewBuffer(reqBody), 12 | ) 13 | if err != nil { 14 | return {{ name }}PutResponse{}, nil, err 15 | } 16 | 17 | req.Header.Add("Content-Type", "application/json") 18 | resp, err := api.httpClient.Do(req) 19 | if err != nil { 20 | return {{ name }}PutResponse{}, nil, err 21 | } 22 | 23 | defer func(Body io.ReadCloser) { 24 | err := Body.Close() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | }(resp.Body) 29 | 30 | respBody, err := io.ReadAll(resp.Body) 31 | if err != nil { 32 | return {{ name }}PutResponse{}, nil, err 33 | } 34 | 35 | if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { 36 | response, err := Unmarshal{{ name }}PutResponse(respBody) 37 | if err != nil { 38 | return {{ name }}PutResponse{}, nil, err 39 | } 40 | 41 | return response, nil, nil 42 | } else { 43 | response, err := UnmarshalErrorResponse(respBody) 44 | if err != nil { 45 | return {{ name }}PutResponse{}, nil, err 46 | } 47 | 48 | return {{ name }}PutResponse{}, &response, errors.New(response.Error) 49 | } 50 | } 51 | "#; 52 | -------------------------------------------------------------------------------- /api/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock title %} - Satounki 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 28 | 29 |
30 |
31 | {% block content %} 32 | {% endblock content %} 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /common/src/policy.rs: -------------------------------------------------------------------------------- 1 | use common_macros::route_request_response; 2 | 3 | use crate::AwsPolicy; 4 | use crate::CloudflareRole; 5 | use crate::GcpRole; 6 | use crate::New; 7 | use crate::Schema; 8 | use crate::Terraform; 9 | 10 | /// Satounki Policy definition 11 | #[apply(New!)] 12 | #[apply(Terraform!)] 13 | #[apply(Schema!)] 14 | pub struct Policy { 15 | /// UUID generated by Satounki 16 | #[schema(example = "5da0230c-4eeb-4840-9ffa-d97f45a12182")] 17 | pub id: String, 18 | /// Succinct, descriptive name for the policy in snake_case 19 | #[schema(example = "compute_dns_admin")] 20 | pub name: String, 21 | /// Description of the permissions granted by this policy 22 | #[schema( 23 | example = "Admin access to AWS and GCP compute resources, and Cloudflare DNS resources" 24 | )] 25 | pub description: String, 26 | /// Google Cloud Platform roles associated with this policy 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub gcp: Option>, 29 | /// Amazon Web Services policy ARNs associated with this policy 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub aws: Option>, 32 | /// Cloudflare roles associated with this policy 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub cloudflare: Option>, 35 | } 36 | 37 | route_request_response! { 38 | #[Put] Policy(NewPolicy) -> Policy, 39 | #[Post] Policy(NewPolicy) -> Policy, 40 | #[Get] Policy() -> Policy, 41 | #[Get] PolicyName() -> Policy, 42 | #[Get] Policies() -> Vec, 43 | } 44 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/aws_account_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | ) 10 | 11 | func (r *awsAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 12 | resp.Schema = schema.Schema{ 13 | Description: resourceDoc(awsAccountResourceData{}), 14 | Attributes: map[string]schema.Attribute{ 15 | "id": schema.StringAttribute{ 16 | Description: fieldDoc(awsAccountResourceData{}, "id"), 17 | Computed: true, 18 | PlanModifiers: []planmodifier.String{ 19 | stringplanmodifier.UseStateForUnknown(), 20 | }, 21 | }, 22 | "last_updated": schema.StringAttribute{ 23 | Description: fieldDoc(awsAccountResourceData{}, "last_updated"), 24 | Computed: true, 25 | }, 26 | "account": schema.StringAttribute{ 27 | Description: fieldDoc(awsAccountResourceData{}, "account"), 28 | Required: true, 29 | }, 30 | "approvals_required": schema.Int64Attribute{ 31 | Description: fieldDoc(awsAccountResourceData{}, "approvals_required"), 32 | Required: true, 33 | }, 34 | "admin_approval_required": schema.BoolAttribute{ 35 | Description: fieldDoc(awsAccountResourceData{}, "admin_approval_required"), 36 | Required: true, 37 | }, 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/gcp_project_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | ) 10 | 11 | func (r *gcpProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 12 | resp.Schema = schema.Schema{ 13 | Description: resourceDoc(gcpProjectResourceData{}), 14 | Attributes: map[string]schema.Attribute{ 15 | "id": schema.StringAttribute{ 16 | Description: fieldDoc(gcpProjectResourceData{}, "id"), 17 | Computed: true, 18 | PlanModifiers: []planmodifier.String{ 19 | stringplanmodifier.UseStateForUnknown(), 20 | }, 21 | }, 22 | "last_updated": schema.StringAttribute{ 23 | Description: fieldDoc(gcpProjectResourceData{}, "last_updated"), 24 | Computed: true, 25 | }, 26 | "project": schema.StringAttribute{ 27 | Description: fieldDoc(gcpProjectResourceData{}, "project"), 28 | Required: true, 29 | }, 30 | "approvals_required": schema.Int64Attribute{ 31 | Description: fieldDoc(gcpProjectResourceData{}, "approvals_required"), 32 | Required: true, 33 | }, 34 | "admin_approval_required": schema.BoolAttribute{ 35 | Description: fieldDoc(gcpProjectResourceData{}, "admin_approval_required"), 36 | Required: true, 37 | }, 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /terraform-providers/satounki/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "terraform-provider-satounki/internal/provider" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 10 | ) 11 | 12 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 13 | 14 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 15 | // ensure the documentation is formatted properly. 16 | //go:generate terraform fmt -recursive ./examples/ 17 | 18 | // Run the docs generation tool, check its repository for more information on how it works and how docs 19 | // can be customized. 20 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 21 | 22 | var ( 23 | // these will be set by the goreleaser configuration 24 | // to appropriate values for the compiled binary 25 | version string = "dev" 26 | 27 | // goreleaser can also pass the specific commit if you want 28 | // commit string = "" 29 | ) 30 | 31 | func main() { 32 | var debug bool 33 | 34 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 35 | flag.Parse() 36 | 37 | opts := providerserver.ServeOpts{ 38 | // TODO: Update this string with the published name of your provider. 39 | Address: "registry.terraform.io/hashicorp/satounki", 40 | Debug: debug, 41 | } 42 | 43 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 44 | 45 | if err != nil { 46 | log.Fatal(err.Error()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/internal/provider/company_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Company 8 | type companyResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Company"` 11 | // Auto-incrementing integer 12 | ID types.String `tfsdk:"id" rustdoc:"Auto-incrementing integer" resourcedoc:"Company"` 13 | // Name of the company 14 | Name types.String `tfsdk:"name" rustdoc:"Name of the company" resourcedoc:"Company"` 15 | // Email domain of the company (G-Suite etc.) 16 | Domain types.String `tfsdk:"domain" rustdoc:"Email domain of the company (G-Suite etc.)" resourcedoc:"Company"` 17 | // Company root user's email address 18 | RootUserEmail types.String `tfsdk:"root_user_email" rustdoc:"Company root user's email address" resourcedoc:"Company"` 19 | // Company root user's first name 20 | RootUserFirstName types.String `tfsdk:"root_user_first_name" rustdoc:"Company root user's first name" resourcedoc:"Company"` 21 | // Company root user's last name 22 | RootUserLastName types.String `tfsdk:"root_user_last_name" rustdoc:"Company root user's last name" resourcedoc:"Company"` 23 | // Company API token 24 | ApiToken types.String `tfsdk:"api_token" rustdoc:"Company API token" resourcedoc:"Company"` 25 | // Company worker key 26 | WorkerKey types.String `tfsdk:"worker_key" rustdoc:"Company worker key" resourcedoc:"Company"` 27 | } 28 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "terraform-provider-satounkiplatform/internal/provider" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 10 | ) 11 | 12 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 13 | 14 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 15 | // ensure the documentation is formatted properly. 16 | //go:generate terraform fmt -recursive ./examples/ 17 | 18 | // Run the docs generation tool, check its repository for more information on how it works and how docs 19 | // can be customized. 20 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 21 | 22 | var ( 23 | // these will be set by the goreleaser configuration 24 | // to appropriate values for the compiled binary 25 | version string = "dev" 26 | 27 | // goreleaser can also pass the specific commit if you want 28 | // commit string = "" 29 | ) 30 | 31 | func main() { 32 | var debug bool 33 | 34 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 35 | flag.Parse() 36 | 37 | opts := providerserver.ServeOpts{ 38 | // TODO: Update this string with the published name of your provider. 39 | Address: "registry.terraform.io/hashicorp/satounkiplatform", 40 | Debug: debug, 41 | } 42 | 43 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 44 | 45 | if err != nil { 46 | log.Fatal(err.Error()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /api/src/worker/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use actix::Message as ActixMessage; 4 | use actix::Recipient; 5 | use common::ServerMessage; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | pub use server::Server; 9 | pub use session::Session; 10 | 11 | pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); 12 | pub const CLIENT_TIMEOUT: Duration = Duration::from_secs(30); 13 | 14 | mod server; 15 | mod session; 16 | 17 | // The Server type, which has a list of recipients, is what implements 18 | // the handler for this. This is so that it can send a Message to each 19 | // recipient in the list 20 | #[derive(ActixMessage, Serialize, Deserialize)] 21 | #[rtype(result = "()")] 22 | pub struct Outgoing { 23 | pub company_domain: String, 24 | pub msg: ServerMessage, 25 | } 26 | 27 | // The WebSocketSession type, which is how we communicate with the recipient, 28 | // is what implements the handler for this. This is what ultimately gets broadcast, 29 | // and these two pieces together are how we ensure that a message is broadcast to 30 | // anything connected to this websocket server 31 | #[derive(ActixMessage)] 32 | #[rtype(result = "()")] 33 | pub struct Message(pub String); 34 | 35 | #[derive(ActixMessage)] 36 | #[rtype(result = "()")] 37 | pub struct Connect { 38 | pub addr: Recipient, 39 | pub company_domain: String, 40 | } 41 | 42 | #[derive(ActixMessage)] 43 | #[rtype(result = "()")] 44 | pub struct Disconnect { 45 | pub company_domain: String, 46 | } 47 | 48 | #[derive(ActixMessage)] 49 | #[rtype(result = "bool")] 50 | pub struct CheckConnection { 51 | pub company_domain: String, 52 | } 53 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/cloudflare_account_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | ) 10 | 11 | func (r *cloudflareAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 12 | resp.Schema = schema.Schema{ 13 | Description: resourceDoc(cloudflareAccountResourceData{}), 14 | Attributes: map[string]schema.Attribute{ 15 | "id": schema.StringAttribute{ 16 | Description: fieldDoc(cloudflareAccountResourceData{}, "id"), 17 | Computed: true, 18 | PlanModifiers: []planmodifier.String{ 19 | stringplanmodifier.UseStateForUnknown(), 20 | }, 21 | }, 22 | "last_updated": schema.StringAttribute{ 23 | Description: fieldDoc(cloudflareAccountResourceData{}, "last_updated"), 24 | Computed: true, 25 | }, 26 | "account": schema.StringAttribute{ 27 | Description: fieldDoc(cloudflareAccountResourceData{}, "account"), 28 | Required: true, 29 | }, 30 | "approvals_required": schema.Int64Attribute{ 31 | Description: fieldDoc(cloudflareAccountResourceData{}, "approvals_required"), 32 | Required: true, 33 | }, 34 | "admin_approval_required": schema.BoolAttribute{ 35 | Description: fieldDoc(cloudflareAccountResourceData{}, "admin_approval_required"), 36 | Required: true, 37 | }, 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/policy_resource_data.go: -------------------------------------------------------------------------------- 1 | // Generated by satounki/common-gen 2 | 3 | package provider 4 | 5 | import "github.com/hashicorp/terraform-plugin-framework/types" 6 | 7 | // Satounki Policy definition 8 | type policyResourceData struct { 9 | // Time of the last modification to this resource 10 | LastUpdated types.String `tfsdk:"last_updated" rustdoc:"Time of the last modification to this resource" resourcedoc:"Satounki Policy definition"` 11 | // UUID generated by Satounki 12 | ID types.String `tfsdk:"id" rustdoc:"UUID generated by Satounki" resourcedoc:"Satounki Policy definition"` 13 | // Succinct, descriptive name for the policy in snake_case 14 | Name types.String `tfsdk:"name" rustdoc:"Succinct, descriptive name for the policy in snake_case" resourcedoc:"Satounki Policy definition"` 15 | // Description of the permissions granted by this policy 16 | Description types.String `tfsdk:"description" rustdoc:"Description of the permissions granted by this policy" resourcedoc:"Satounki Policy definition"` 17 | // Google Cloud Platform roles associated with this policy 18 | Gcp []types.String `tfsdk:"gcp" rustdoc:"Google Cloud Platform roles associated with this policy" resourcedoc:"Satounki Policy definition"` 19 | // Amazon Web Services policy ARNs associated with this policy 20 | Aws []types.String `tfsdk:"aws" rustdoc:"Amazon Web Services policy ARNs associated with this policy" resourcedoc:"Satounki Policy definition"` 21 | // Cloudflare roles associated with this policy 22 | Cloudflare []types.String `tfsdk:"cloudflare" rustdoc:"Cloudflare roles associated with this policy" resourcedoc:"Satounki Policy definition"` 23 | } 24 | -------------------------------------------------------------------------------- /database/src/api_token.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | 8 | use crate::schema::api_tokens; 9 | use crate::Result; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable, AsChangeset)] 12 | #[diesel(table_name = api_tokens)] 13 | pub struct ApiToken { 14 | pub token: String, 15 | pub company_id: i32, 16 | } 17 | 18 | impl ApiToken { 19 | pub fn create(connection: &mut SqliteConnection, company_id: i32) -> Result { 20 | diesel::insert_into(api_tokens::table) 21 | .values(Self { 22 | token: Uuid::new_v4().to_string(), 23 | company_id, 24 | }) 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, token: &str) -> Result { 29 | api_tokens::table.find(token).first(connection) 30 | } 31 | 32 | pub fn read_by_company_id(connection: &mut SqliteConnection, company_id: i32) -> Result { 33 | api_tokens::table 34 | .filter(api_tokens::dsl::company_id.eq(company_id)) 35 | .first(connection) 36 | } 37 | 38 | pub fn delete(connection: &mut SqliteConnection, company_id: i32) -> Result { 39 | use api_tokens::dsl; 40 | 41 | diesel::delete(dsl::api_tokens.filter(dsl::company_id.eq(company_id))).execute(connection) 42 | } 43 | 44 | pub fn replace(connection: &mut SqliteConnection, company_id: i32) -> Result { 45 | Self::delete(connection, company_id)?; 46 | Self::create(connection, company_id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_aliases_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | ) 10 | 11 | func (r *userAliasesResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 12 | resp.Schema = schema.Schema{ 13 | Description: resourceDoc(userAliasesResourceData{}), 14 | Attributes: map[string]schema.Attribute{ 15 | "id": schema.StringAttribute{ 16 | Description: fieldDoc(userAliasesResourceData{}, "id"), 17 | Computed: true, 18 | PlanModifiers: []planmodifier.String{ 19 | stringplanmodifier.UseStateForUnknown(), 20 | }, 21 | }, 22 | "last_updated": schema.StringAttribute{ 23 | Description: fieldDoc(userAliasesResourceData{}, "last_updated"), 24 | Computed: true, 25 | }, 26 | "email": schema.StringAttribute{ 27 | Description: fieldDoc(userAliasesResourceData{}, "email"), 28 | Required: true, 29 | }, 30 | "aws": schema.StringAttribute{ 31 | Description: fieldDoc(userAliasesResourceData{}, "aws"), 32 | Optional: true, 33 | }, 34 | "cloudflare": schema.StringAttribute{ 35 | Description: fieldDoc(userAliasesResourceData{}, "cloudflare"), 36 | Optional: true, 37 | }, 38 | "gcp": schema.StringAttribute{ 39 | Description: fieldDoc(userAliasesResourceData{}, "gcp"), 40 | Optional: true, 41 | }, 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api/src/settings_gcp_projects.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::web; 4 | use common::GcpProject; 5 | use common::SettingsGcpProjectsGetResponse; 6 | use database::Pool; 7 | 8 | use crate::auth::ApiTokenOrUserWithAccessRole; 9 | use crate::auth::UserRole; 10 | use crate::public_doc::ex; 11 | use crate::Result; 12 | 13 | /// List GCP projects 14 | #[utoipa::path( 15 | context_path = "/v1/settings", 16 | tag = "settings", 17 | security(("user_token" = []), ("api_token" = [])), 18 | responses( 19 | (status = 200, body = SettingsGcpProjectsGetResponse), 20 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 21 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 22 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 23 | ) 24 | )] 25 | #[get("/gcp-projects")] 26 | async fn settings_gcp_projects_get( 27 | pool: web::Data, 28 | authenticated: ApiTokenOrUserWithAccessRole, 29 | ) -> Result> { 30 | let connection = &mut *pool.get()?; 31 | let company = authenticated.information().company(connection)?; 32 | let results = company.gcp_projects(connection)?; 33 | 34 | Ok(web::Json(SettingsGcpProjectsGetResponse( 35 | results 36 | .into_iter() 37 | .map(|project| GcpProject { 38 | id: project.id, 39 | project: project.gcp_project, 40 | approvals_required: project.approvals_required, 41 | admin_approval_required: project.admin_approval_required, 42 | }) 43 | .collect(), 44 | ))) 45 | } 46 | -------------------------------------------------------------------------------- /database/src/user_company_role.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::users_companies_roles; 8 | use crate::Result; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 11 | #[diesel(table_name = users_companies_roles)] 12 | pub struct UserCompanyRole { 13 | pub user_id: i32, 14 | pub company_id: i32, 15 | pub role_id: i32, 16 | } 17 | 18 | impl UserCompanyRole { 19 | pub fn create(connection: &mut SqliteConnection, user_company_role: Self) -> Result { 20 | diesel::insert_into(users_companies_roles::table) 21 | .values(user_company_role) 22 | .get_result(connection) 23 | } 24 | 25 | pub fn delete_for_user( 26 | connection: &mut SqliteConnection, 27 | user_id: i32, 28 | company_id: i32, 29 | ) -> Result { 30 | use users_companies_roles::dsl; 31 | 32 | diesel::delete( 33 | dsl::users_companies_roles 34 | .filter(dsl::user_id.eq(user_id)) 35 | .filter(dsl::company_id.eq(company_id)), 36 | ) 37 | .execute(connection) 38 | } 39 | 40 | pub fn delete( 41 | connection: &mut SqliteConnection, 42 | user_id: i32, 43 | company_id: i32, 44 | role_id: i32, 45 | ) -> Result { 46 | use users_companies_roles::dsl; 47 | 48 | diesel::delete( 49 | dsl::users_companies_roles 50 | .filter(dsl::user_id.eq(user_id)) 51 | .filter(dsl::role_id.eq(role_id)) 52 | .filter(dsl::company_id.eq(company_id)), 53 | ) 54 | .execute(connection) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /api/src/settings_aws_accounts.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::web; 4 | use common::AwsAccount; 5 | use common::SettingsAwsAccountsGetResponse; 6 | use database::Pool; 7 | 8 | use crate::auth::ApiTokenOrUserWithAccessRole; 9 | use crate::auth::UserRole; 10 | use crate::public_doc::ex; 11 | use crate::Result; 12 | 13 | /// List AWS accounts 14 | #[utoipa::path( 15 | context_path = "/v1/settings", 16 | tag = "settings", 17 | security(("user_token" = []), ("api_token" = [])), 18 | responses( 19 | (status = 200, body = SettingsAwsAccountsGetResponse), 20 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 21 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 22 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 23 | ) 24 | )] 25 | #[get("/aws-accounts")] 26 | async fn settings_aws_accounts_get( 27 | pool: web::Data, 28 | authenticated: ApiTokenOrUserWithAccessRole, 29 | ) -> Result> { 30 | let connection = &mut *pool.get()?; 31 | let company = authenticated.information().company(connection)?; 32 | let results = company.aws_accounts(&mut *connection)?; 33 | 34 | Ok(web::Json(SettingsAwsAccountsGetResponse( 35 | results 36 | .into_iter() 37 | .map(|account| AwsAccount { 38 | id: account.id, 39 | account: account.aws_account_alias, 40 | approvals_required: account.approvals_required, 41 | admin_approval_required: account.admin_approval_required, 42 | }) 43 | .collect(), 44 | ))) 45 | } 46 | -------------------------------------------------------------------------------- /api/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Settings{% endblock title %} 3 | 4 | {% block navigation %} 5 | 8 | 11 | 14 | 17 | 20 | {% endblock navigation %} 21 | 22 | {% block content %} 23 |
24 |
25 |

{{ company.name }}

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% if api_token %} 41 | 42 | 43 | 44 | 45 | {% endif %} 46 | 47 |
Domain{{ company.domain }}
Root User{{ company.root_user }}
User Token{{ user_token }}
API Token{{ api_token }}
48 |
49 |
50 | {% endblock content %} -------------------------------------------------------------------------------- /database/src/worker_key.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | 8 | use crate::schema::worker_keys; 9 | use crate::Result; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable, AsChangeset)] 12 | #[diesel(table_name = worker_keys)] 13 | pub struct WorkerKey { 14 | pub company_id: i32, 15 | pub key: String, 16 | } 17 | 18 | impl WorkerKey { 19 | pub fn create(connection: &mut SqliteConnection, company_id: i32) -> Result { 20 | diesel::insert_into(worker_keys::table) 21 | .values(Self { 22 | company_id, 23 | key: format!("swk-{}", Uuid::new_v4()), 24 | }) 25 | .get_result(connection) 26 | } 27 | 28 | pub fn read(connection: &mut SqliteConnection, company_id: i32) -> Result> { 29 | worker_keys::table 30 | .filter(worker_keys::dsl::company_id.eq(company_id)) 31 | .first(connection) 32 | .optional() 33 | } 34 | 35 | pub fn read_by_company_id(connection: &mut SqliteConnection, company_id: i32) -> Result { 36 | worker_keys::table 37 | .filter(worker_keys::dsl::company_id.eq(company_id)) 38 | .first(connection) 39 | } 40 | 41 | pub fn delete(connection: &mut SqliteConnection, company_id: i32) -> Result { 42 | use worker_keys::dsl; 43 | 44 | diesel::delete(dsl::worker_keys.filter(dsl::company_id.eq(company_id))).execute(connection) 45 | } 46 | 47 | pub fn replace(connection: &mut SqliteConnection, company_id: i32) -> Result { 48 | Self::delete(connection, company_id)?; 49 | Self::create(connection, company_id) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /database/src/company_policy.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::company_policies; 8 | use crate::Result; 9 | 10 | #[derive( 11 | Debug, Clone, Serialize, Deserialize, Identifiable, Insertable, Queryable, AsChangeset, 12 | )] 13 | #[diesel(table_name = company_policies)] 14 | pub struct CompanyPolicy { 15 | pub id: String, 16 | pub company_id: i32, 17 | pub name: String, 18 | pub policy: String, 19 | pub description: String, 20 | } 21 | 22 | impl CompanyPolicy { 23 | pub fn create(connection: &mut SqliteConnection, policy: &Self) -> Result { 24 | diesel::insert_into(company_policies::table) 25 | .values(policy) 26 | .get_result(connection) 27 | } 28 | 29 | pub fn read(connection: &mut SqliteConnection, id: &str) -> Result { 30 | company_policies::dsl::company_policies 31 | .find(id) 32 | .first(connection) 33 | } 34 | 35 | pub fn read_by_name( 36 | connection: &mut SqliteConnection, 37 | name: &str, 38 | company_id: i32, 39 | ) -> Result { 40 | company_policies::table 41 | .filter(company_policies::dsl::company_id.eq(company_id)) 42 | .filter(company_policies::dsl::name.eq(name)) 43 | .first(connection) 44 | } 45 | 46 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 47 | self.save_changes(connection) 48 | } 49 | 50 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 51 | use company_policies::dsl; 52 | 53 | diesel::delete(dsl::company_policies.filter(dsl::id.eq(&self.id))).execute(connection) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/src/settings_cf_accounts.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::web; 4 | use common::CloudflareAccount; 5 | use common::SettingsCloudflareAccountsGetResponse; 6 | use database::Pool; 7 | 8 | use crate::auth::ApiTokenOrUserWithAccessRole; 9 | use crate::auth::UserRole; 10 | use crate::public_doc::ex; 11 | use crate::Result; 12 | 13 | /// List CF accounts 14 | #[utoipa::path( 15 | context_path = "/v1/settings", 16 | tag = "settings", 17 | security(("user_token" = []), ("api_token" = [])), 18 | responses( 19 | (status = 200, body = SettingsCloudflareAccountsGetResponse), 20 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 21 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 22 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 23 | ) 24 | )] 25 | #[get("/cloudflare-accounts")] 26 | async fn settings_cf_accounts_get( 27 | pool: web::Data, 28 | authenticated: ApiTokenOrUserWithAccessRole, 29 | ) -> Result> { 30 | let connection = &mut *pool.get()?; 31 | let company = authenticated.information().company(connection)?; 32 | let results = company.cloudflare_accounts(&mut *connection)?; 33 | 34 | Ok(web::Json(SettingsCloudflareAccountsGetResponse( 35 | results 36 | .into_iter() 37 | .map(|account| CloudflareAccount { 38 | id: account.id, 39 | account: account.cloudflare_account_alias, 40 | approvals_required: account.approvals_required, 41 | admin_approval_required: account.admin_approval_required, 42 | }) 43 | .collect(), 44 | ))) 45 | } 46 | -------------------------------------------------------------------------------- /api/src/policies.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::web; 4 | use common::NewPolicy; 5 | use common::PoliciesGetResponse; 6 | use common::Policy; 7 | use database::Pool; 8 | 9 | use crate::auth::ApiTokenOrUserWithAccessRole; 10 | use crate::auth::UserRole; 11 | use crate::public_doc::ex; 12 | use crate::Result; 13 | 14 | /// List policies 15 | #[utoipa::path( 16 | context_path = "/v1", 17 | tag = "policies", 18 | security(("user_token" = []), ("api_token" = [])), 19 | responses( 20 | (status = 200, body = PoliciesGetResponse), 21 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 22 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 23 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 24 | ) 25 | )] 26 | #[get("/policies")] 27 | async fn policies_get( 28 | pool: web::Data, 29 | authenticated: ApiTokenOrUserWithAccessRole, 30 | ) -> Result> { 31 | let connection = &mut *pool.get()?; 32 | 33 | let user = authenticated.information().user(connection)?; 34 | let company = user.company(connection)?; 35 | let policies = company.policies(connection)?; 36 | 37 | let mut parsed = vec![]; 38 | for policy in policies { 39 | let partially_parsed: NewPolicy = serde_json::from_str(&policy.policy)?; 40 | parsed.push(Policy { 41 | id: policy.id.clone(), 42 | description: policy.description.clone(), 43 | name: policy.name.clone(), 44 | gcp: partially_parsed.gcp, 45 | aws: partially_parsed.aws, 46 | cloudflare: partially_parsed.cloudflare, 47 | }); 48 | } 49 | 50 | Ok(web::Json(PoliciesGetResponse(parsed))) 51 | } 52 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_roles_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "satounki" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" 9 | ) 10 | 11 | func (d userRolesResourceData) PostRequest() satounki.UserRolesPostRequest { 12 | var roles []satounki.AccessRole 13 | for _, r := range d.AccessRoles { 14 | roles = append(roles, satounki.AccessRole(r.ValueString())) 15 | } 16 | 17 | return satounki.UserRolesPostRequest(roles) 18 | } 19 | 20 | func (d *userRolesResourceData) PostResponse(r satounki.UserRolesPostResponse) { 21 | var roles []types.String 22 | for _, role := range r { 23 | roles = append(roles, types.StringValue(string(role))) 24 | } 25 | 26 | d.ID = types.StringValue(id.UniqueId()) 27 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 28 | d.AccessRoles = roles 29 | } 30 | 31 | func (d userRolesResourceData) PutRequest() satounki.UserRolesPutRequest { 32 | var roles []satounki.AccessRole 33 | for _, r := range d.AccessRoles { 34 | roles = append(roles, satounki.AccessRole(r.ValueString())) 35 | } 36 | 37 | return satounki.UserRolesPutRequest(roles) 38 | } 39 | 40 | func (d *userRolesResourceData) PutResponse(r satounki.UserRolesPutResponse) { 41 | var roles []types.String 42 | for _, role := range r { 43 | roles = append(roles, types.StringValue(string(role))) 44 | } 45 | 46 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 47 | d.AccessRoles = roles 48 | } 49 | 50 | func (d *userRolesResourceData) GetResponse(r satounki.UserRolesGetResponse) { 51 | var roles []types.String 52 | for _, role := range r { 53 | roles = append(roles, types.StringValue(string(role))) 54 | } 55 | 56 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 57 | d.AccessRoles = roles 58 | } 59 | -------------------------------------------------------------------------------- /database/src/gcp_request.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::gcp_requests; 8 | use crate::Result; 9 | 10 | #[derive( 11 | Debug, Clone, Serialize, Deserialize, Insertable, Identifiable, Queryable, AsChangeset, 12 | )] 13 | #[diesel(table_name = gcp_requests)] 14 | #[diesel(primary_key(access_request_id, user, project, role))] 15 | pub struct GcpRequest { 16 | pub access_request_id: String, 17 | pub company_id: i32, 18 | pub user: String, 19 | pub project: String, 20 | pub role: String, 21 | } 22 | 23 | impl GcpRequest { 24 | pub fn create(connection: &mut SqliteConnection, batch: &Vec) -> Result { 25 | diesel::insert_into(gcp_requests::table) 26 | .values(batch) 27 | .execute(connection) 28 | } 29 | 30 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result> { 31 | gcp_requests::table 32 | .filter(gcp_requests::dsl::access_request_id.eq(access_request_id)) 33 | .load::<_>(connection) 34 | } 35 | 36 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 37 | self.save_changes(connection) 38 | } 39 | 40 | pub fn delete(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 41 | use gcp_requests::dsl; 42 | 43 | diesel::delete(dsl::gcp_requests.filter(dsl::access_request_id.eq(access_request_id))) 44 | .execute(connection) 45 | } 46 | 47 | pub fn replace( 48 | connection: &mut SqliteConnection, 49 | access_request_id: &str, 50 | batch: &Vec, 51 | ) -> Result { 52 | Self::delete(connection, access_request_id)?; 53 | Self::create(connection, batch) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /database/src/aws_request.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::aws_requests; 8 | use crate::Result; 9 | 10 | #[derive( 11 | Debug, Clone, Serialize, Deserialize, Identifiable, Insertable, Queryable, AsChangeset, 12 | )] 13 | #[diesel(table_name = aws_requests)] 14 | #[diesel(primary_key(access_request_id, user, account_alias, role))] 15 | pub struct AwsRequest { 16 | pub access_request_id: String, 17 | pub company_id: i32, 18 | pub user: String, 19 | pub account_alias: String, 20 | pub role: String, 21 | } 22 | 23 | impl AwsRequest { 24 | pub fn create(connection: &mut SqliteConnection, batch: &Vec) -> Result { 25 | diesel::insert_into(aws_requests::table) 26 | .values(batch) 27 | .execute(connection) 28 | } 29 | 30 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result> { 31 | aws_requests::table 32 | .filter(aws_requests::dsl::access_request_id.eq(access_request_id)) 33 | .load::<_>(connection) 34 | } 35 | 36 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 37 | self.save_changes(connection) 38 | } 39 | 40 | pub fn delete(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 41 | use aws_requests::dsl; 42 | 43 | diesel::delete(dsl::aws_requests.filter(dsl::access_request_id.eq(access_request_id))) 44 | .execute(connection) 45 | } 46 | 47 | pub fn replace( 48 | connection: &mut SqliteConnection, 49 | access_request_id: &str, 50 | batch: &Vec, 51 | ) -> Result { 52 | Self::delete(connection, access_request_id)?; 53 | Self::create(connection, batch) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /database/src/company_slack.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use diesel::prelude::*; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | 7 | use crate::schema::company_slack; 8 | use crate::Result; 9 | 10 | #[derive( 11 | Debug, Clone, Serialize, Deserialize, Identifiable, Insertable, Queryable, AsChangeset, 12 | )] 13 | #[diesel(table_name = company_slack)] 14 | #[diesel(primary_key(company_id))] 15 | pub struct CompanySlack { 16 | pub company_id: i32, 17 | pub team_id: String, 18 | pub team_name: String, 19 | pub channel_id: String, 20 | pub access_token: String, 21 | pub incoming_webhook: String, 22 | } 23 | 24 | impl CompanySlack { 25 | pub fn create(connection: &mut SqliteConnection, slack: Self) -> Result { 26 | diesel::insert_into(company_slack::table) 27 | .values(slack.clone()) 28 | .on_conflict(company_slack::dsl::company_id) 29 | .do_update() 30 | .set(slack) 31 | .get_result(connection) 32 | } 33 | 34 | pub fn read(connection: &mut SqliteConnection, id: i32) -> Result { 35 | company_slack::dsl::company_slack.find(id).first(connection) 36 | } 37 | 38 | pub fn read_by_team_id(connection: &mut SqliteConnection, team_id: &str) -> Result { 39 | company_slack::dsl::company_slack 40 | .filter(company_slack::dsl::team_id.eq(team_id)) 41 | .first(connection) 42 | } 43 | 44 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 45 | self.save_changes(connection) 46 | } 47 | 48 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 49 | use company_slack::dsl; 50 | 51 | diesel::delete(dsl::company_slack.filter(dsl::company_id.eq(&self.company_id))) 52 | .execute(connection) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cli/src/reporters.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::owo_colors::OwoColorize; 2 | use tabled::settings::object::Columns; 3 | use tabled::settings::object::Rows; 4 | use tabled::settings::Disable; 5 | use tabled::settings::Format; 6 | use tabled::settings::Modify; 7 | use tabled::settings::Style; 8 | use tabled::Table; 9 | 10 | pub fn error(errors: &[&str]) { 11 | let data: Vec<_> = errors.iter().map(|e| ("error", *e)).collect(); 12 | 13 | eprintln!( 14 | "{}", 15 | Table::new(data) 16 | .with(Style::blank()) 17 | .with(Disable::row(Rows::new(..1))) 18 | .with(Modify::new(Columns::single(0)).with(Format::content(|s| s.red().to_string())),) 19 | .with(Modify::new(Columns::single(1)).with(Format::content(|s| s.red().to_string())),) 20 | ); 21 | 22 | std::process::exit(1) 23 | } 24 | 25 | #[derive(Copy, Clone)] 26 | pub enum Outcome { 27 | Positive, 28 | Negative, 29 | Neutral, 30 | } 31 | 32 | pub fn outcome(outcome: Outcome, message: &[(&str, &str)]) { 33 | println!( 34 | "{}", 35 | Table::new(message) 36 | .with(Style::blank()) 37 | .with(Disable::row(Rows::new(..1))) 38 | .with(Modify::new(Columns::single(0)).with(Format::content(|s| { 39 | match outcome { 40 | Outcome::Positive => s.green().to_string(), 41 | Outcome::Negative => s.red().to_string(), 42 | Outcome::Neutral => s.bright_blue().to_string(), 43 | } 44 | }))) 45 | .with(Modify::new(Columns::single(1)).with(Format::content(|s| { 46 | match outcome { 47 | Outcome::Positive => s.green().to_string(), 48 | Outcome::Negative => s.red().to_string(), 49 | Outcome::Neutral => s.bright_blue().to_string(), 50 | } 51 | }))) 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::nursery, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | use clap::Parser; 5 | use serde::Deserialize; 6 | 7 | use crate::cli::Cli; 8 | use crate::client::Client; 9 | use crate::configuration::Configuration; 10 | 11 | mod cli; 12 | mod client; 13 | mod configuration; 14 | mod request; 15 | 16 | pub const SKIP_CHECKS: bool = true; 17 | pub const DRY_RUN: bool = true; 18 | 19 | #[tokio::main] 20 | async fn main() -> color_eyre::Result<()> { 21 | if std::env::var("RUST_LOG").is_err() { 22 | std::env::set_var("RUST_LOG", "info"); 23 | } 24 | 25 | env_logger::init(); 26 | color_eyre::install()?; 27 | 28 | let cli = Cli::parse(); 29 | let configuration: Configuration = 30 | serde_yaml::from_str(&std::fs::read_to_string(&cli.config)?)?; 31 | 32 | let company_domain = 33 | std::env::var("COMPANY_DOMAIN").unwrap_or_else(|_| "satounki.com".to_string()); 34 | let company_worker_key = std::env::var("COMPANY_WORKER_KEY") 35 | .unwrap_or_else(|_| "swk-e0c43bd0-38a4-4e7b-9c0f-8bd5f47f20d2".to_string()); 36 | let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()); 37 | 38 | let mut client = Client::new( 39 | company_domain, 40 | company_worker_key, 41 | configuration, 42 | port.parse()?, 43 | ) 44 | .await?; 45 | client.listen().await?; 46 | 47 | Ok(()) 48 | } 49 | 50 | #[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)] 51 | pub struct GcpServiceAccount { 52 | #[serde(rename = "type")] 53 | pub type_field: String, 54 | pub project_id: String, 55 | pub private_key_id: String, 56 | pub private_key: String, 57 | pub client_email: String, 58 | pub client_id: String, 59 | pub auth_uri: String, 60 | pub token_uri: String, 61 | pub auth_provider_x509_cert_url: String, 62 | pub client_x509_cert_url: String, 63 | } 64 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/policy_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | ) 12 | 13 | func (r *policyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 14 | resp.Schema = schema.Schema{ 15 | Description: resourceDoc(policyResourceData{}), 16 | Attributes: map[string]schema.Attribute{ 17 | "id": schema.StringAttribute{ 18 | Description: fieldDoc(policyResourceData{}, "id"), 19 | Computed: true, 20 | PlanModifiers: []planmodifier.String{ 21 | stringplanmodifier.UseStateForUnknown(), 22 | }, 23 | }, 24 | "last_updated": schema.StringAttribute{ 25 | Description: fieldDoc(policyResourceData{}, "last_updated"), 26 | Computed: true, 27 | }, 28 | "name": schema.StringAttribute{ 29 | Description: fieldDoc(policyResourceData{}, "name"), 30 | Required: true, 31 | }, 32 | "description": schema.StringAttribute{ 33 | Description: fieldDoc(policyResourceData{}, "description"), 34 | Required: true, 35 | }, 36 | "aws": schema.ListAttribute{ 37 | ElementType: types.StringType, 38 | Description: fieldDoc(policyResourceData{}, "aws"), 39 | Optional: true, 40 | }, 41 | "cloudflare": schema.ListAttribute{ 42 | ElementType: types.StringType, 43 | Description: fieldDoc(policyResourceData{}, "cloudflare"), 44 | Optional: true, 45 | }, 46 | "gcp": schema.ListAttribute{ 47 | ElementType: types.StringType, 48 | Description: fieldDoc(policyResourceData{}, "gcp"), 49 | Optional: true, 50 | }, 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/src/cloudflare_request.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use common::CloudflareRole; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::cloudflare_requests; 9 | use crate::Result; 10 | 11 | #[derive( 12 | Debug, Clone, Serialize, Deserialize, Insertable, Identifiable, Queryable, AsChangeset, 13 | )] 14 | #[diesel(table_name = cloudflare_requests)] 15 | #[diesel(primary_key(access_request_id, user, role))] 16 | pub struct CloudflareRequest { 17 | pub access_request_id: String, 18 | pub company_id: i32, 19 | pub user: String, 20 | pub account_alias: String, 21 | pub role: CloudflareRole, 22 | } 23 | 24 | impl CloudflareRequest { 25 | pub fn create(connection: &mut SqliteConnection, batch: &Vec) -> Result { 26 | diesel::insert_into(cloudflare_requests::table) 27 | .values(batch) 28 | .execute(connection) 29 | } 30 | 31 | pub fn read(connection: &mut SqliteConnection, access_request_id: &str) -> Result> { 32 | cloudflare_requests::table 33 | .filter(cloudflare_requests::dsl::access_request_id.eq(access_request_id)) 34 | .load::<_>(connection) 35 | } 36 | 37 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 38 | self.save_changes(connection) 39 | } 40 | 41 | pub fn delete(connection: &mut SqliteConnection, access_request_id: &str) -> Result { 42 | use cloudflare_requests::dsl; 43 | 44 | diesel::delete( 45 | dsl::cloudflare_requests.filter(dsl::access_request_id.eq(access_request_id)), 46 | ) 47 | .execute(connection) 48 | } 49 | 50 | pub fn replace( 51 | connection: &mut SqliteConnection, 52 | access_request_id: &str, 53 | batch: &Vec, 54 | ) -> Result { 55 | Self::delete(connection, access_request_id)?; 56 | Self::create(connection, batch) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cloudflare/src/tokens.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use serde_json::Value; 6 | 7 | use crate::CloudflareResponse; 8 | 9 | impl CloudflareResponse for VerifyTokenResponse {} 10 | 11 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 12 | pub struct VerifyTokenResponse { 13 | pub result: Result, 14 | pub success: bool, 15 | pub errors: Vec, 16 | pub messages: Vec, 17 | } 18 | 19 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 20 | pub struct Result { 21 | pub id: String, 22 | pub status: String, 23 | } 24 | 25 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 26 | pub struct Message { 27 | pub code: i64, 28 | pub message: String, 29 | #[serde(rename = "type")] 30 | pub type_field: Value, 31 | } 32 | 33 | impl CloudflareResponse for TokenDetailsResponse {} 34 | 35 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 36 | pub struct TokenDetailsResponse { 37 | pub success: bool, 38 | pub errors: Vec, 39 | pub messages: Vec, 40 | pub result: ResultField, 41 | } 42 | 43 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 44 | pub struct ResultField { 45 | pub id: String, 46 | pub name: String, 47 | pub status: String, 48 | pub issued_on: String, 49 | pub modified_on: String, 50 | pub not_before: String, 51 | pub expires_on: String, 52 | pub policies: Vec, 53 | pub condition: HashMap, 54 | } 55 | 56 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 57 | pub struct Policy { 58 | pub id: String, 59 | pub effect: String, 60 | pub resources: HashMap, 61 | pub permission_groups: Vec, 62 | } 63 | 64 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 65 | pub struct PermissionGroup { 66 | pub id: String, 67 | pub name: String, 68 | } 69 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/aws_account_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "satounki" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | ) 9 | 10 | func (d awsAccountResourceData) PostRequest() satounki.SettingsAwsAccountPostRequest { 11 | return satounki.SettingsAwsAccountPostRequest{ 12 | Account: d.Account.ValueString(), 13 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 14 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 15 | } 16 | } 17 | 18 | func (d *awsAccountResourceData) PostResponse(r satounki.SettingsAwsAccountPostResponse) { 19 | d.ID = types.StringValue(r.ID) 20 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 21 | d.Account = types.StringValue(r.Account) 22 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 23 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 24 | } 25 | 26 | func (d awsAccountResourceData) PutRequest() satounki.SettingsAwsAccountPutRequest { 27 | return satounki.SettingsAwsAccountPutRequest{ 28 | Account: d.Account.ValueString(), 29 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 30 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 31 | } 32 | } 33 | 34 | func (d *awsAccountResourceData) PutResponse(r satounki.SettingsAwsAccountPutResponse) { 35 | d.ID = types.StringValue(r.ID) 36 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 37 | d.Account = types.StringValue(r.Account) 38 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 39 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 40 | } 41 | 42 | func (d *awsAccountResourceData) GetResponse(r satounki.SettingsAwsAccountGetResponse) { 43 | d.ID = types.StringValue(r.ID) 44 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 45 | d.Account = types.StringValue(r.Account) 46 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 47 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 48 | } 49 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/gcp_project_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "satounki" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | ) 9 | 10 | func (d gcpProjectResourceData) PostRequest() satounki.SettingsGcpProjectPostRequest { 11 | return satounki.SettingsGcpProjectPostRequest{ 12 | Project: d.Project.ValueString(), 13 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 14 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 15 | } 16 | } 17 | 18 | func (d *gcpProjectResourceData) PostResponse(r satounki.SettingsGcpProjectPostResponse) { 19 | d.ID = types.StringValue(r.ID) 20 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 21 | d.Project = types.StringValue(r.Project) 22 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 23 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 24 | } 25 | 26 | func (d gcpProjectResourceData) PutRequest() satounki.SettingsGcpProjectPutRequest { 27 | return satounki.SettingsGcpProjectPutRequest{ 28 | Project: d.Project.ValueString(), 29 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 30 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 31 | } 32 | } 33 | 34 | func (d *gcpProjectResourceData) PutResponse(r satounki.SettingsGcpProjectPutResponse) { 35 | d.ID = types.StringValue(r.ID) 36 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 37 | d.Project = types.StringValue(r.Project) 38 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 39 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 40 | } 41 | 42 | func (d *gcpProjectResourceData) GetResponse(r satounki.SettingsGcpProjectGetResponse) { 43 | d.ID = types.StringValue(r.ID) 44 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 45 | d.Project = types.StringValue(r.Project) 46 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 47 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 48 | } 49 | -------------------------------------------------------------------------------- /terraform-providers/satounki/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: '{{ .CommitTimestamp }}' 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - '386' 26 | - arm 27 | - arm64 28 | ignore: 29 | - goos: darwin 30 | goarch: '386' 31 | binary: '{{ .ProjectName }}_v{{ .Version }}' 32 | archives: 33 | - format: zip 34 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 35 | checksum: 36 | extra_files: 37 | - glob: 'terraform-registry-manifest.json' 38 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 40 | algorithm: sha256 41 | signs: 42 | - artifacts: checksum 43 | args: 44 | # if you are using this in a GitHub action or some other automated pipeline, you 45 | # need to pass the batch flag to indicate its not interactive. 46 | - "--batch" 47 | - "--local-user" 48 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 49 | - "--output" 50 | - "${signature}" 51 | - "--detach-sign" 52 | - "${artifact}" 53 | release: 54 | extra_files: 55 | - glob: 'terraform-registry-manifest.json' 56 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 57 | # If you want to manually examine the release before its live, uncomment this line: 58 | # draft: true 59 | changelog: 60 | skip: true 61 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: '{{ .CommitTimestamp }}' 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - '386' 26 | - arm 27 | - arm64 28 | ignore: 29 | - goos: darwin 30 | goarch: '386' 31 | binary: '{{ .ProjectName }}_v{{ .Version }}' 32 | archives: 33 | - format: zip 34 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 35 | checksum: 36 | extra_files: 37 | - glob: 'terraform-registry-manifest.json' 38 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 40 | algorithm: sha256 41 | signs: 42 | - artifacts: checksum 43 | args: 44 | # if you are using this in a GitHub action or some other automated pipeline, you 45 | # need to pass the batch flag to indicate its not interactive. 46 | - "--batch" 47 | - "--local-user" 48 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 49 | - "--output" 50 | - "${signature}" 51 | - "--detach-sign" 52 | - "${artifact}" 53 | release: 54 | extra_files: 55 | - glob: 'terraform-registry-manifest.json' 56 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 57 | # If you want to manually examine the release before its live, uncomment this line: 58 | # draft: true 59 | changelog: 60 | skip: true 61 | -------------------------------------------------------------------------------- /api/src/auth/platform_token_with_scope.rs: -------------------------------------------------------------------------------- 1 | use actix::fut::ready; 2 | use actix::fut::Ready; 3 | use actix_web::dev::Payload; 4 | use actix_web::web; 5 | use actix_web::FromRequest; 6 | use actix_web::HttpMessage; 7 | use actix_web::HttpRequest; 8 | use actix_web_httpauth::extractors::bearer::BearerAuth; 9 | use color_eyre::eyre::anyhow; 10 | use database::PlatformToken; 11 | use database::Pool; 12 | 13 | use crate::auth::platform_token_scope::PlatformTokenScope; 14 | use crate::error; 15 | 16 | pub struct PlatformTokenWithScope { 17 | phantom_data: std::marker::PhantomData, 18 | } 19 | 20 | impl FromRequest for PlatformTokenWithScope { 21 | type Error = actix_web::Error; 22 | type Future = Ready>; 23 | 24 | fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { 25 | let token = match req.extensions().get::() { 26 | None => return ready(Err(error::Api::UnauthorizedPlatformTokenRequired.into())), 27 | Some(credentials) => credentials.token().to_string(), 28 | }; 29 | 30 | let mut connection = match req.app_data::>() { 31 | None => return ready(Err(error::Api::Other(anyhow!("database error")).into())), 32 | Some(pool) => match pool.get() { 33 | Ok(connection) => connection, 34 | Err(error) => return ready(Err(error::Api::DatabaseConnection(error).into())), 35 | }, 36 | }; 37 | 38 | if let Ok(platform_token) = PlatformToken::read(&mut connection, &token) { 39 | if platform_token.scope == S::as_enum() 40 | || platform_token.scope == common_platform::PlatformTokenScope::Write 41 | { 42 | return ready(Ok(Self { 43 | phantom_data: std::marker::PhantomData, 44 | })); 45 | } 46 | } 47 | 48 | ready(Err(error::Api::UnauthorizedPlatformScopeRequired( 49 | S::as_str(), 50 | ) 51 | .into())) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/src/user_alias.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use common::UserAliases; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::user_aliases; 9 | use crate::Result; 10 | 11 | #[derive( 12 | Default, Debug, Clone, Serialize, Deserialize, Insertable, Identifiable, Queryable, AsChangeset, 13 | )] 14 | #[diesel(table_name = user_aliases)] 15 | #[diesel(primary_key(user_id))] 16 | #[diesel(treat_none_as_null = true)] 17 | pub struct UserAlias { 18 | pub user_id: i32, 19 | pub aws: Option, 20 | pub cloudflare: Option, 21 | pub gcp: Option, 22 | } 23 | 24 | impl From for UserAliases { 25 | fn from(u: UserAlias) -> Self { 26 | Self { 27 | aws: u.aws, 28 | cloudflare: u.cloudflare, 29 | gcp: u.gcp, 30 | } 31 | } 32 | } 33 | 34 | impl UserAlias { 35 | pub fn create(connection: &mut SqliteConnection, user_alias: &Self) -> Result { 36 | diesel::insert_into(user_aliases::table) 37 | .values(user_alias) 38 | .on_conflict(user_aliases::dsl::user_id) 39 | .do_update() 40 | .set(user_alias) 41 | .get_result(connection) 42 | } 43 | 44 | pub fn read(connection: &mut SqliteConnection, user_id: i32) -> Result { 45 | user_aliases::dsl::user_aliases 46 | .find(user_id) 47 | .first(connection) 48 | } 49 | 50 | pub fn read_optional(connection: &mut SqliteConnection, user_id: i32) -> Result> { 51 | user_aliases::dsl::user_aliases 52 | .find(user_id) 53 | .first(connection) 54 | .optional() 55 | } 56 | 57 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 58 | self.save_changes(connection) 59 | } 60 | 61 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 62 | use user_aliases::dsl; 63 | 64 | diesel::delete(dsl::user_aliases.filter(dsl::user_id.eq(self.user_id))).execute(connection) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/cloudflare_account_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "satounki" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | ) 9 | 10 | func (d cloudflareAccountResourceData) PostRequest() satounki.SettingsCloudflareAccountPostRequest { 11 | return satounki.SettingsCloudflareAccountPostRequest{ 12 | Account: d.Account.ValueString(), 13 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 14 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 15 | } 16 | } 17 | 18 | func (d *cloudflareAccountResourceData) PostResponse(r satounki.SettingsCloudflareAccountPostResponse) { 19 | d.ID = types.StringValue(r.ID) 20 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 21 | d.Account = types.StringValue(r.Account) 22 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 23 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 24 | } 25 | 26 | func (d cloudflareAccountResourceData) PutRequest() satounki.SettingsCloudflareAccountPutRequest { 27 | return satounki.SettingsCloudflareAccountPutRequest{ 28 | Account: d.Account.ValueString(), 29 | AdminApprovalRequired: d.AdminApprovalRequired.ValueBool(), 30 | ApprovalsRequired: d.ApprovalsRequired.ValueInt64(), 31 | } 32 | } 33 | 34 | func (d *cloudflareAccountResourceData) PutResponse(r satounki.SettingsCloudflareAccountPutResponse) { 35 | d.ID = types.StringValue(r.ID) 36 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 37 | d.Account = types.StringValue(r.Account) 38 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 39 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 40 | } 41 | 42 | func (d *cloudflareAccountResourceData) GetResponse(r satounki.SettingsCloudflareAccountGetResponse) { 43 | d.ID = types.StringValue(r.ID) 44 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 45 | d.Account = types.StringValue(r.Account) 46 | d.AdminApprovalRequired = types.BoolValue(r.AdminApprovalRequired) 47 | d.ApprovalsRequired = types.Int64Value(r.ApprovalsRequired) 48 | } 49 | -------------------------------------------------------------------------------- /api/src/worker/server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use actix::Actor; 5 | use actix::Context; 6 | use actix::Handler; 7 | use actix::Recipient; 8 | use parking_lot::Mutex; 9 | 10 | use super::CheckConnection; 11 | use super::Connect; 12 | use super::Disconnect; 13 | use super::Message; 14 | use super::Outgoing; 15 | 16 | #[derive(Clone)] 17 | pub struct Server { 18 | pub sessions: Arc>>>, 19 | } 20 | 21 | impl Actor for Server { 22 | type Context = Context; 23 | } 24 | 25 | impl Handler for Server { 26 | type Result = (); 27 | 28 | #[allow(clippy::significant_drop_tightening)] 29 | fn handle(&mut self, out: Outgoing, _: &mut Self::Context) -> Self::Result { 30 | let sessions = self.sessions.lock(); 31 | 32 | if let Some(recipient) = sessions.get(&out.company_domain) { 33 | log::debug!("[{}] sending message to worker", out.company_domain); 34 | recipient.do_send(Message(out.msg.to_string())); 35 | } else { 36 | log::warn!("[{}] no worker connected", out.company_domain); 37 | }; 38 | } 39 | } 40 | 41 | impl Handler for Server { 42 | type Result = (); 43 | 44 | fn handle(&mut self, msg: Connect, _: &mut Context) { 45 | log::info!("[{}] adding worker websocket session", msg.company_domain); 46 | let mut sessions = self.sessions.lock(); 47 | sessions.insert(msg.company_domain.clone(), msg.addr); 48 | } 49 | } 50 | 51 | impl Handler for Server { 52 | type Result = (); 53 | 54 | fn handle(&mut self, msg: Disconnect, _: &mut Context) { 55 | log::info!("[{}] removing worker websocket session", msg.company_domain,); 56 | let mut sessions = self.sessions.lock(); 57 | sessions.remove(&msg.company_domain); 58 | } 59 | } 60 | 61 | impl Handler for Server { 62 | type Result = bool; 63 | 64 | fn handle(&mut self, msg: CheckConnection, _: &mut Context) -> Self::Result { 65 | let sessions = self.sessions.lock(); 66 | sessions.contains_key(&msg.company_domain) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /api/src/requests.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::web; 4 | use common::RequestsGetQueryParams; 5 | use common::RequestsGetResponse; 6 | use database::Pool; 7 | use database::RequestWrapper; 8 | 9 | use crate::auth::ApiTokenOrUserWithAccessRole; 10 | use crate::auth::UserRole; 11 | use crate::error; 12 | use crate::public_doc::ex; 13 | use crate::Result; 14 | 15 | /// List requests 16 | /// 17 | /// The state of the request must be specified. 18 | /// 19 | /// The number of requests to return must be specified. 20 | /// 21 | /// The maximum number of matching requests that can be returned is 20. 22 | #[utoipa::path( 23 | context_path = "/v1", 24 | tag = "requests", 25 | security(("user_token" = []), ("api_token" = [])), 26 | responses( 27 | (status = 200, body = RequestsGetResponse), 28 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 29 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 30 | (status = 422, body = ErrorResponse, example = json!(ex(StatusCode::UNPROCESSABLE_ENTITY))), 31 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 32 | ) 33 | )] 34 | #[get("/requests")] 35 | async fn requests_get( 36 | pool: web::Data, 37 | authenticated: ApiTokenOrUserWithAccessRole, 38 | query: web::Query, 39 | ) -> Result> { 40 | const MAXIMUM_REQUEST_COUNT: i64 = 20; 41 | 42 | if query.count > MAXIMUM_REQUEST_COUNT { 43 | return Err(error::Api::MaximumRequestListExceeded( 44 | MAXIMUM_REQUEST_COUNT, 45 | )); 46 | } 47 | 48 | let connection = &mut *pool.get()?; 49 | 50 | let company = authenticated.information().company(connection)?; 51 | let ids = company.access_request_ids(connection, query.state, query.count)?; 52 | 53 | let mut responses = vec![]; 54 | let requests = RequestWrapper::read_all(connection, &ids)?; 55 | for request in requests { 56 | responses.push(request); 57 | } 58 | 59 | Ok(web::Json(RequestsGetResponse(responses))) 60 | } 61 | -------------------------------------------------------------------------------- /api/src/auth/api_token_or_user_with_access_role.rs: -------------------------------------------------------------------------------- 1 | use actix::fut::ready; 2 | use actix::fut::Ready; 3 | use actix_web::dev::Payload; 4 | use actix_web::Either; 5 | use actix_web::FromRequest; 6 | use actix_web::HttpRequest; 7 | use oauth2::http::StatusCode; 8 | 9 | use crate::auth; 10 | use crate::auth::access_role::AccessRole; 11 | use crate::auth::authenticated::Authenticated; 12 | use crate::auth::user_with_access_role::UserWithAccessRole; 13 | use crate::error; 14 | 15 | #[derive(derive_more::Deref)] 16 | pub struct ApiTokenOrUserWithAccessRole( 17 | Either>, 18 | ); 19 | 20 | impl ApiTokenOrUserWithAccessRole { 21 | pub const fn information(&self) -> &Authenticated { 22 | match &self.0 { 23 | Either::Left(token) => &token.0, 24 | Either::Right(user) => &user.authenticated, 25 | } 26 | } 27 | } 28 | 29 | impl FromRequest for ApiTokenOrUserWithAccessRole { 30 | type Error = actix_web::Error; 31 | type Future = Ready>; 32 | 33 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 34 | let mut decision = Err(error::Api::UnauthorizedApiTokenOrRoleRequired(R::as_str()).into()); 35 | 36 | match auth::ApiToken::from_request(req, payload).into_inner() { 37 | Ok(auth) => { 38 | decision = Ok(Self(Either::Left(auth))); 39 | } 40 | Err(error) => { 41 | if error.error_response().status() == StatusCode::FORBIDDEN { 42 | decision = Err(error); 43 | } 44 | } 45 | } 46 | 47 | if decision.is_ok() { 48 | return ready(decision); 49 | } 50 | 51 | match UserWithAccessRole::::from_request(req, payload).into_inner() { 52 | Ok(auth) => { 53 | decision = Ok(Self(Either::Right(auth))); 54 | } 55 | Err(error) => { 56 | if error.error_response().status() == StatusCode::FORBIDDEN { 57 | decision = Err(error); 58 | } 59 | } 60 | } 61 | 62 | ready(decision) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /database/src/platform_token.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes)] 2 | 3 | use common_platform::PlatformTokenScope; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | use uuid::Uuid; 8 | 9 | use crate::schema::platform_tokens; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable, AsChangeset)] 13 | #[diesel(table_name = platform_tokens)] 14 | pub struct PlatformToken { 15 | pub token: String, 16 | pub scope: PlatformTokenScope, 17 | } 18 | 19 | impl PlatformToken { 20 | pub fn create(connection: &mut SqliteConnection, scope: PlatformTokenScope) -> Result { 21 | diesel::insert_into(platform_tokens::table) 22 | .values(Self { 23 | token: Uuid::new_v4().to_string(), 24 | scope, 25 | }) 26 | .get_result(connection) 27 | } 28 | 29 | pub fn create_from_env( 30 | connection: &mut SqliteConnection, 31 | scope: PlatformTokenScope, 32 | token: &str, 33 | ) -> Result { 34 | diesel::insert_into(platform_tokens::table) 35 | .values(Self { 36 | token: token.to_string(), 37 | scope, 38 | }) 39 | .get_result(connection) 40 | } 41 | 42 | pub fn read(connection: &mut SqliteConnection, token: &str) -> Result { 43 | platform_tokens::table.find(token).first(connection) 44 | } 45 | 46 | pub fn read_by_scope( 47 | connection: &mut SqliteConnection, 48 | scope: PlatformTokenScope, 49 | ) -> Result { 50 | use platform_tokens::dsl; 51 | 52 | platform_tokens::table 53 | .filter(dsl::scope.eq(scope)) 54 | .first(connection) 55 | } 56 | 57 | pub fn delete(connection: &mut SqliteConnection, scope: PlatformTokenScope) -> Result { 58 | use platform_tokens::dsl; 59 | 60 | diesel::delete(dsl::platform_tokens.filter(dsl::scope.eq(scope))).execute(connection) 61 | } 62 | 63 | pub fn replace(connection: &mut SqliteConnection, scope: PlatformTokenScope) -> Result { 64 | Self::delete(connection, scope)?; 65 | Self::create(connection, scope) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /database/src/company_gcp_project.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use common::GcpProject; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::company_gcp_projects; 9 | use crate::Result; 10 | 11 | #[derive( 12 | Debug, Clone, Serialize, Deserialize, Insertable, Identifiable, Queryable, AsChangeset, 13 | )] 14 | #[diesel(table_name = company_gcp_projects)] 15 | pub struct CompanyGcpProject { 16 | pub id: String, 17 | pub company_id: i32, 18 | pub gcp_project: String, 19 | pub approvals_required: i32, 20 | pub admin_approval_required: bool, 21 | } 22 | 23 | impl From for GcpProject { 24 | fn from(p: CompanyGcpProject) -> Self { 25 | GcpProject { 26 | id: p.id, 27 | project: p.gcp_project, 28 | approvals_required: p.approvals_required, 29 | admin_approval_required: p.admin_approval_required, 30 | } 31 | } 32 | } 33 | 34 | impl CompanyGcpProject { 35 | pub fn create(connection: &mut SqliteConnection, project: Self) -> Result { 36 | diesel::insert_into(company_gcp_projects::table) 37 | .values(project) 38 | .on_conflict_do_nothing() 39 | .get_result(connection) 40 | } 41 | 42 | pub fn read(connection: &mut SqliteConnection, id: &str) -> Result { 43 | company_gcp_projects::dsl::company_gcp_projects 44 | .find(id) 45 | .first(connection) 46 | } 47 | 48 | pub fn read_by_project( 49 | connection: &mut SqliteConnection, 50 | name: &str, 51 | company_id: i32, 52 | ) -> Result { 53 | company_gcp_projects::table 54 | .filter(company_gcp_projects::dsl::company_id.eq(company_id)) 55 | .filter(company_gcp_projects::dsl::gcp_project.eq(name)) 56 | .first(connection) 57 | } 58 | 59 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 60 | self.save_changes(connection) 61 | } 62 | 63 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 64 | use company_gcp_projects::dsl; 65 | 66 | diesel::delete(dsl::company_gcp_projects.filter(dsl::id.eq(&self.id))).execute(connection) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /database/src/company_aws_account.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use common::AwsAccount; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::company_aws_accounts; 9 | use crate::Result; 10 | 11 | #[derive( 12 | Debug, Clone, Serialize, Deserialize, Identifiable, Insertable, Queryable, AsChangeset, 13 | )] 14 | #[diesel(table_name = company_aws_accounts)] 15 | pub struct CompanyAwsAccount { 16 | pub id: String, 17 | pub company_id: i32, 18 | pub aws_account_alias: String, 19 | pub approvals_required: i32, 20 | pub admin_approval_required: bool, 21 | } 22 | 23 | impl From for AwsAccount { 24 | fn from(a: CompanyAwsAccount) -> Self { 25 | Self { 26 | id: a.id, 27 | account: a.aws_account_alias, 28 | approvals_required: a.approvals_required, 29 | admin_approval_required: a.admin_approval_required, 30 | } 31 | } 32 | } 33 | 34 | impl CompanyAwsAccount { 35 | pub fn create(connection: &mut SqliteConnection, account: Self) -> Result { 36 | diesel::insert_into(company_aws_accounts::table) 37 | .values(account) 38 | .on_conflict_do_nothing() 39 | .get_result(connection) 40 | } 41 | 42 | pub fn read(connection: &mut SqliteConnection, id: &str) -> Result { 43 | company_aws_accounts::dsl::company_aws_accounts 44 | .find(id) 45 | .first(connection) 46 | } 47 | 48 | pub fn read_by_alias( 49 | connection: &mut SqliteConnection, 50 | name: &str, 51 | company_id: i32, 52 | ) -> Result { 53 | company_aws_accounts::table 54 | .filter(company_aws_accounts::dsl::company_id.eq(company_id)) 55 | .filter(company_aws_accounts::dsl::aws_account_alias.eq(name)) 56 | .first(connection) 57 | } 58 | 59 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 60 | self.save_changes(connection) 61 | } 62 | 63 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 64 | use company_aws_accounts::dsl; 65 | 66 | diesel::delete(dsl::company_aws_accounts.filter(dsl::id.eq(&self.id))).execute(connection) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /api/src/user_token.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::put; 4 | use actix_web::web; 5 | use common::UserTokenGetResponse; 6 | use common::UserTokenPutResponse; 7 | use database::Pool; 8 | use database::UserToken; 9 | 10 | use crate::auth::UserRole; 11 | use crate::auth::UserWithAccessRole; 12 | use crate::public_doc::ex; 13 | use crate::Result; 14 | 15 | /// View user token 16 | #[utoipa::path( 17 | context_path = "/v1/user", 18 | tag = "users", 19 | security(("user_token" = []), ("api_token" = [])), 20 | responses( 21 | (status = 200, body = UserTokenGetResponse), 22 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 23 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 24 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 25 | ) 26 | )] 27 | #[get("/token")] 28 | async fn user_token_get( 29 | pool: web::Data, 30 | authenticated: UserWithAccessRole, 31 | ) -> Result> { 32 | let connection = &mut *pool.get()?; 33 | let token = authenticated 34 | .user(connection)? 35 | .user_token(connection)? 36 | .token; 37 | 38 | Ok(web::Json(UserTokenGetResponse(common::UserToken { token }))) 39 | } 40 | 41 | /// Regenerate user token 42 | #[utoipa::path( 43 | context_path = "/v1/user", 44 | tag = "users", 45 | security(("user_token" = []), ("api_token" = [])), 46 | responses( 47 | (status = 200, body = UserTokenPutResponse), 48 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 49 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 50 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 51 | ) 52 | )] 53 | #[put("/token")] 54 | async fn user_token_put( 55 | pool: web::Data, 56 | authenticated: UserWithAccessRole, 57 | ) -> Result> { 58 | let connection = &mut *pool.get()?; 59 | let user_id = authenticated.user_id()?; 60 | let token = UserToken::replace(connection, user_id)?.token; 61 | 62 | Ok(web::Json(UserTokenPutResponse(common::UserToken { token }))) 63 | } 64 | -------------------------------------------------------------------------------- /database/src/company_cloudflare_account.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::extra_unused_lifetimes, clippy::use_self)] 2 | 3 | use common::CloudflareAccount; 4 | use diesel::prelude::*; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::schema::company_cloudflare_accounts; 9 | use crate::Result; 10 | 11 | #[derive( 12 | Debug, Clone, Serialize, Deserialize, Identifiable, Insertable, Queryable, AsChangeset, 13 | )] 14 | #[diesel(table_name = company_cloudflare_accounts)] 15 | pub struct CompanyCloudflareAccount { 16 | pub id: String, 17 | pub company_id: i32, 18 | pub cloudflare_account_alias: String, 19 | pub approvals_required: i32, 20 | pub admin_approval_required: bool, 21 | } 22 | 23 | impl From for CloudflareAccount { 24 | fn from(a: CompanyCloudflareAccount) -> Self { 25 | Self { 26 | id: a.id, 27 | account: a.cloudflare_account_alias, 28 | approvals_required: a.approvals_required, 29 | admin_approval_required: a.admin_approval_required, 30 | } 31 | } 32 | } 33 | 34 | impl CompanyCloudflareAccount { 35 | pub fn create(connection: &mut SqliteConnection, account: Self) -> Result { 36 | diesel::insert_into(company_cloudflare_accounts::table) 37 | .values(account) 38 | .on_conflict_do_nothing() 39 | .get_result(connection) 40 | } 41 | 42 | pub fn read(connection: &mut SqliteConnection, id: &str) -> Result { 43 | company_cloudflare_accounts::dsl::company_cloudflare_accounts 44 | .find(id) 45 | .first(connection) 46 | } 47 | 48 | pub fn read_by_alias( 49 | connection: &mut SqliteConnection, 50 | name: &str, 51 | company_id: i32, 52 | ) -> Result { 53 | company_cloudflare_accounts::table 54 | .filter(company_cloudflare_accounts::dsl::company_id.eq(company_id)) 55 | .filter(company_cloudflare_accounts::dsl::cloudflare_account_alias.eq(name)) 56 | .first(connection) 57 | } 58 | 59 | pub fn update(&self, connection: &mut SqliteConnection) -> Result { 60 | self.save_changes(connection) 61 | } 62 | 63 | pub fn delete(&self, connection: &mut SqliteConnection) -> Result { 64 | use company_cloudflare_accounts::dsl; 65 | 66 | diesel::delete(dsl::company_cloudflare_accounts.filter(dsl::id.eq(&self.id))) 67 | .execute(connection) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/internal/provider/company_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-framework/resource" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 9 | ) 10 | 11 | // Schema defines the schema for the resource. 12 | func (r *companyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 13 | resp.Schema = schema.Schema{ 14 | Description: resourceDoc(companyResourceData{}), 15 | Attributes: map[string]schema.Attribute{ 16 | "id": schema.StringAttribute{ 17 | Description: fieldDoc(companyResourceData{}, "id"), 18 | Computed: true, 19 | PlanModifiers: []planmodifier.String{ 20 | stringplanmodifier.UseStateForUnknown(), 21 | }, 22 | }, 23 | "last_updated": schema.StringAttribute{ 24 | Description: fieldDoc(companyResourceData{}, "last_updated"), 25 | Computed: true, 26 | }, 27 | "name": schema.StringAttribute{ 28 | Description: fieldDoc(companyResourceData{}, "name"), 29 | Required: true, 30 | }, 31 | "domain": schema.StringAttribute{ 32 | Description: fieldDoc(companyResourceData{}, "domain"), 33 | Required: true, 34 | }, 35 | "root_user_email": schema.StringAttribute{ 36 | Description: fieldDoc(companyResourceData{}, "root_user_email"), 37 | Required: true, 38 | }, 39 | "root_user_first_name": schema.StringAttribute{ 40 | Description: fieldDoc(companyResourceData{}, "root_user_first_name"), 41 | Required: true, 42 | }, 43 | "root_user_last_name": schema.StringAttribute{ 44 | Description: fieldDoc(companyResourceData{}, "root_user_last_name"), 45 | Required: true, 46 | }, 47 | "api_token": schema.StringAttribute{ 48 | Description: fieldDoc(companyResourceData{}, "api_token"), 49 | Computed: true, 50 | Sensitive: true, 51 | PlanModifiers: []planmodifier.String{ 52 | stringplanmodifier.UseStateForUnknown(), 53 | }, 54 | }, 55 | "worker_key": schema.StringAttribute{ 56 | Description: fieldDoc(companyResourceData{}, "worker_key"), 57 | Computed: true, 58 | Sensitive: true, 59 | PlanModifiers: []planmodifier.String{ 60 | stringplanmodifier.UseStateForUnknown(), 61 | }, 62 | }, 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rolescraper/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::nursery, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | use aws::Aws; 5 | use color_eyre::Result; 6 | use gcloud::Gcloud; 7 | use serde::Serialize; 8 | 9 | #[derive(Serialize)] 10 | pub struct AwsPolicy { 11 | path: String, 12 | policy_name: String, 13 | policy_id: String, 14 | arn: String, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<()> { 19 | let mut gcloud = Gcloud::iam().roles().list().format("json"); 20 | let output = gcloud.output()?; 21 | std::fs::write("rolescraper_gcp.json", output.stdout)?; 22 | 23 | let aws = Aws::new( 24 | &std::env::var("AWS_ACCESS_KEY_ID")?, 25 | &std::env::var("AWS_SECRET_ACCESS_KEY")?, 26 | ); 27 | 28 | let mut all_aws_policies = vec![]; 29 | 30 | let mut aws_policies = aws.list_policies(None, None, Some(1000)).await?; 31 | if let Some(policies) = aws_policies.policies { 32 | for policy in policies { 33 | if let Some(arn) = &policy.arn { 34 | if arn.starts_with("arn:aws:iam::aws:policy") && policy.is_attachable { 35 | all_aws_policies.push(AwsPolicy { 36 | path: policy.path.unwrap(), 37 | policy_name: policy.policy_name.unwrap(), 38 | policy_id: policy.policy_id.unwrap(), 39 | arn: arn.clone(), 40 | }); 41 | } 42 | } 43 | } 44 | } 45 | 46 | while aws_policies.is_truncated { 47 | aws_policies = aws 48 | .list_policies(None, aws_policies.marker, Some(1000)) 49 | .await?; 50 | 51 | if let Some(policies) = aws_policies.policies { 52 | for policy in policies { 53 | if let Some(arn) = &policy.arn { 54 | if arn.starts_with("arn:aws:iam::aws:policy") && policy.is_attachable { 55 | all_aws_policies.push(AwsPolicy { 56 | path: policy.path.unwrap(), 57 | policy_name: policy.policy_name.unwrap(), 58 | policy_id: policy.policy_id.unwrap(), 59 | arn: arn.clone(), 60 | }); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | std::fs::write( 68 | "rolescraper_aws.json", 69 | serde_json::to_string_pretty(&all_aws_policies)?, 70 | )?; 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /api/src/settings_token.rs: -------------------------------------------------------------------------------- 1 | use actix_web::get; 2 | use actix_web::http::StatusCode; 3 | use actix_web::put; 4 | use actix_web::web; 5 | use common::SettingsTokenGetResponse; 6 | use common::SettingsTokenPutResponse; 7 | use database::ApiToken; 8 | use database::Pool; 9 | 10 | use crate::auth::AdministratorRole; 11 | use crate::auth::ApiTokenOrUserWithAccessRole; 12 | use crate::public_doc::ex; 13 | use crate::Result; 14 | 15 | /// View API token 16 | #[utoipa::path( 17 | context_path = "/v1/settings", 18 | tag = "settings", 19 | security(("user_token" = []), ("api_token" = [])), 20 | responses( 21 | (status = 200, body = SettingsTokenGetResponse), 22 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 23 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 24 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 25 | ) 26 | )] 27 | #[get("/token")] 28 | async fn settings_token_get( 29 | pool: web::Data, 30 | authenticated: ApiTokenOrUserWithAccessRole, 31 | ) -> Result> { 32 | let connection = &mut *pool.get()?; 33 | let token = authenticated 34 | .information() 35 | .company(connection)? 36 | .api_token(connection)? 37 | .token; 38 | 39 | Ok(web::Json(SettingsTokenGetResponse(common::ApiToken { 40 | token, 41 | }))) 42 | } 43 | 44 | /// Regenerate API token 45 | #[utoipa::path( 46 | context_path = "/v1/settings", 47 | tag = "settings", 48 | security(("user_token" = []), ("api_token" = [])), 49 | responses( 50 | (status = 200, body = SettingsTokenPutResponse), 51 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 52 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 53 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 54 | ) 55 | )] 56 | #[put("/token")] 57 | async fn settings_token_put( 58 | pool: web::Data, 59 | authenticated: ApiTokenOrUserWithAccessRole, 60 | ) -> Result> { 61 | let connection = &mut *pool.get()?; 62 | let company_id = authenticated.information().company_id(connection)?; 63 | let token = ApiToken::replace(connection, company_id)?.token; 64 | 65 | Ok(web::Json(SettingsTokenPutResponse(common::ApiToken { 66 | token, 67 | }))) 68 | } 69 | -------------------------------------------------------------------------------- /api/src/platform_token.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use actix_web::get; 4 | use actix_web::http::StatusCode; 5 | use actix_web::put; 6 | use actix_web::web; 7 | use common_platform::PlatformTokenGetResponse; 8 | use common_platform::PlatformTokenPutResponse; 9 | use common_platform::PlatformTokenScope; 10 | use database::PlatformToken; 11 | use database::Pool; 12 | 13 | use crate::auth::PlatformTokenWithScope; 14 | use crate::auth::Read; 15 | use crate::auth::Write; 16 | use crate::public_doc::ex; 17 | use crate::Result; 18 | 19 | /// View platform token 20 | #[utoipa::path( 21 | context_path = "/platform", 22 | tag = "tokens", 23 | security(("platform_token" = [])), 24 | responses( 25 | (status = 200, body = PlatformTokenGetResponse), 26 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 27 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 28 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 29 | ) 30 | )] 31 | #[get("/token/{scope}")] 32 | async fn platform_token_get( 33 | pool: web::Data, 34 | _platform: PlatformTokenWithScope, 35 | scope: web::Path, 36 | ) -> Result> { 37 | let connection = &mut *pool.get()?; 38 | let scope = PlatformTokenScope::from_str(&*scope)?; 39 | let token = PlatformToken::read_by_scope(connection, scope)?.token; 40 | 41 | Ok(web::Json(PlatformTokenGetResponse( 42 | common_platform::PlatformToken { token }, 43 | ))) 44 | } 45 | 46 | /// Regenerate platform token 47 | #[utoipa::path( 48 | context_path = "/platform", 49 | tag = "tokens", 50 | security(("platform_token" = [])), 51 | responses( 52 | (status = 200, body = PlatformTokenPutResponse), 53 | (status = 401, body = ErrorResponse, example = json!(ex(StatusCode::UNAUTHORIZED))), 54 | (status = 404, body = ErrorResponse, example = json!(ex(StatusCode::NOT_FOUND))), 55 | (status = 500, body = ErrorResponse, example = json!(ex(StatusCode::INTERNAL_SERVER_ERROR))), 56 | ) 57 | )] 58 | #[put("/token/{scope}")] 59 | async fn platform_token_put( 60 | pool: web::Data, 61 | _platform: PlatformTokenWithScope, 62 | scope: web::Path, 63 | ) -> Result> { 64 | let connection = &mut *pool.get()?; 65 | 66 | let scope = PlatformTokenScope::from_str(&*scope)?; 67 | let token = PlatformToken::replace(connection, scope)?.token; 68 | 69 | Ok(web::Json(PlatformTokenPutResponse( 70 | common_platform::PlatformToken { token }, 71 | ))) 72 | } 73 | -------------------------------------------------------------------------------- /terraform-providers/satounki/internal/provider/user_aliases_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "satounki" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" 9 | ) 10 | 11 | func (d userAliasesResourceData) PostRequest() satounki.UserAliasesPostRequest { 12 | return satounki.UserAliasesPostRequest{ 13 | Aws: d.Aws.ValueStringPointer(), 14 | Cloudflare: d.Cloudflare.ValueStringPointer(), 15 | Gcp: d.Gcp.ValueStringPointer(), 16 | } 17 | } 18 | 19 | func (d *userAliasesResourceData) PostResponse(r satounki.UserAliasesPostResponse) { 20 | d.ID = types.StringValue(id.UniqueId()) 21 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 22 | 23 | if r.Aws != nil && *r.Aws != "" { 24 | d.Aws = types.StringValue(*r.Aws) 25 | } else { 26 | d.Aws = types.StringNull() 27 | } 28 | 29 | if r.Cloudflare != nil && *r.Cloudflare != "" { 30 | d.Cloudflare = types.StringValue(*r.Cloudflare) 31 | } else { 32 | d.Cloudflare = types.StringNull() 33 | } 34 | 35 | if r.Gcp != nil && *r.Gcp != "" { 36 | d.Gcp = types.StringValue(*r.Gcp) 37 | } else { 38 | d.Gcp = types.StringNull() 39 | } 40 | } 41 | 42 | func (d userAliasesResourceData) PutRequest() satounki.UserAliasesPutRequest { 43 | return satounki.UserAliasesPutRequest{ 44 | Aws: d.Aws.ValueStringPointer(), 45 | Cloudflare: d.Cloudflare.ValueStringPointer(), 46 | Gcp: d.Gcp.ValueStringPointer(), 47 | } 48 | } 49 | 50 | func (d *userAliasesResourceData) PutResponse(r satounki.UserAliasesPutResponse) { 51 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 52 | 53 | if r.Aws != nil && *r.Aws != "" { 54 | d.Aws = types.StringValue(*r.Aws) 55 | } else { 56 | d.Aws = types.StringNull() 57 | } 58 | 59 | if r.Cloudflare != nil && *r.Cloudflare != "" { 60 | d.Cloudflare = types.StringValue(*r.Cloudflare) 61 | } else { 62 | d.Cloudflare = types.StringNull() 63 | } 64 | 65 | if r.Gcp != nil && *r.Gcp != "" { 66 | d.Gcp = types.StringValue(*r.Gcp) 67 | } else { 68 | d.Gcp = types.StringNull() 69 | } 70 | } 71 | 72 | func (d *userAliasesResourceData) GetResponse(r satounki.UserAliasesGetResponse) { 73 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 74 | 75 | if r.Aws != nil && *r.Aws != "" { 76 | d.Aws = types.StringValue(*r.Aws) 77 | } else { 78 | d.Aws = types.StringNull() 79 | } 80 | 81 | if r.Cloudflare != nil && *r.Cloudflare != "" { 82 | d.Cloudflare = types.StringValue(*r.Cloudflare) 83 | } else { 84 | d.Cloudflare = types.StringNull() 85 | } 86 | 87 | if r.Gcp != nil && *r.Gcp != "" { 88 | d.Gcp = types.StringValue(*r.Gcp) 89 | } else { 90 | d.Gcp = types.StringNull() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /common-gen/src/terraform.rs: -------------------------------------------------------------------------------- 1 | use common::AccessRole; 2 | use common::AwsAccount; 3 | use common::CloudflareAccount; 4 | use common::GcpProject; 5 | use common::Policy; 6 | use common::UserAliases; 7 | use common_platform::Company; 8 | 9 | pub struct Resource<'a> { 10 | pub name: &'a str, 11 | pub doc: &'a str, 12 | pub members: Vec, 13 | pub identifier: &'a str, 14 | pub api_prefix: &'a str, 15 | pub has_post_id: bool, 16 | } 17 | 18 | pub fn platform_resources<'a>() -> Vec> { 19 | vec![Resource { 20 | name: "company", 21 | doc: Company::terraform_resource_members().0, 22 | members: Company::terraform_resource_members().1, 23 | identifier: "ID", 24 | api_prefix: "", 25 | has_post_id: false, 26 | }] 27 | } 28 | 29 | pub fn resources<'a>() -> Vec> { 30 | vec![ 31 | Resource { 32 | name: "policy", 33 | doc: Policy::terraform_resource_members().0, 34 | members: Policy::terraform_resource_members().1, 35 | identifier: "ID", 36 | api_prefix: "", 37 | has_post_id: false, 38 | }, 39 | Resource { 40 | name: "aws_account", 41 | doc: AwsAccount::terraform_resource_members().0, 42 | members: AwsAccount::terraform_resource_members().1, 43 | identifier: "ID", 44 | api_prefix: "Settings", 45 | has_post_id: false, 46 | }, 47 | Resource { 48 | name: "cloudflare_account", 49 | doc: CloudflareAccount::terraform_resource_members().0, 50 | members: CloudflareAccount::terraform_resource_members().1, 51 | identifier: "ID", 52 | api_prefix: "Settings", 53 | has_post_id: false, 54 | }, 55 | Resource { 56 | name: "gcp_project", 57 | doc: GcpProject::terraform_resource_members().0, 58 | members: GcpProject::terraform_resource_members().1, 59 | identifier: "ID", 60 | api_prefix: "Settings", 61 | has_post_id: false, 62 | }, 63 | Resource { 64 | name: "user_aliases", 65 | doc: UserAliases::terraform_resource_members().0, 66 | members: UserAliases::terraform_resource_members().1, 67 | identifier: "Email", 68 | api_prefix: "", 69 | has_post_id: true, 70 | }, 71 | Resource { 72 | name: "user_roles", 73 | doc: AccessRole::terraform_resource_members().0, 74 | members: AccessRole::terraform_resource_members().1, 75 | identifier: "Email", 76 | api_prefix: "", 77 | has_post_id: true, 78 | }, 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /api/templates/users.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Users{% endblock title %} 3 | 4 | {% block navigation %} 5 | 8 | 11 | 14 | 17 | 20 | {% endblock navigation %} 21 | 22 | {% block content %} 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for user in users %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
NameEmailActiveRoles
{{ user.first_name }} {{ user.last_name }}{{ user.email }}{{ user.active }}{{ user.roles | join(sep=", ") }}
46 |
47 |
48 |
49 | 50 |
51 |

Filter by Role

52 | User 53 | Approver 54 | Administrator 55 |
56 |

Filter by Status

57 | 58 | Active 59 | Disabled 60 |
61 |
62 |
63 | {% endblock content %} -------------------------------------------------------------------------------- /api/src/platform_doc.rs: -------------------------------------------------------------------------------- 1 | use actix_web::http::StatusCode; 2 | // use common::*; 3 | use common_platform::CompaniesGetResponse; 4 | use common_platform::Company; 5 | use common_platform::CompanyGetResponse; 6 | use common_platform::CompanyPostRequest; 7 | use common_platform::CompanyPostResponse; 8 | use common_platform::CompanyPutRequest; 9 | use common_platform::CompanyPutResponse; 10 | use common_platform::ErrorResponse; 11 | use common_platform::PlatformToken; 12 | use common_platform::PlatformTokenGetResponse; 13 | use common_platform::PlatformTokenPutResponse; 14 | use utoipa::openapi::security::Http; 15 | use utoipa::openapi::security::HttpAuthScheme; 16 | use utoipa::openapi::security::SecurityScheme; 17 | use utoipa::Modify; 18 | use utoipa::OpenApi; 19 | 20 | use crate::company; 21 | use crate::platform_token; 22 | 23 | pub fn ex(code: StatusCode) -> ErrorResponse { 24 | ErrorResponse { 25 | code: code.as_u16(), 26 | error: code.to_string().replace(&format!("{} ", code.as_u16()), ""), 27 | } 28 | } 29 | 30 | #[derive(OpenApi)] 31 | #[openapi( 32 | modifiers(&SecurityAddon), 33 | paths( 34 | company::companies_get, 35 | company::company_get, 36 | company::company_post, 37 | company::company_put, 38 | company::company_delete, 39 | platform_token::platform_token_get, 40 | platform_token::platform_token_put, 41 | ), 42 | components( 43 | responses( 44 | CompaniesGetResponse, 45 | CompanyGetResponse, 46 | CompanyPostResponse, 47 | CompanyPutResponse, 48 | PlatformTokenGetResponse, 49 | PlatformTokenPutResponse, 50 | ), 51 | schemas( 52 | // these need to be duplicated here 53 | CompaniesGetResponse, 54 | CompanyGetResponse, 55 | CompanyPostResponse, 56 | CompanyPutResponse, 57 | PlatformTokenGetResponse, 58 | PlatformTokenPutResponse, 59 | 60 | Company, 61 | CompanyPostRequest, 62 | CompanyPutRequest, 63 | ErrorResponse, 64 | PlatformToken, 65 | ) 66 | ) 67 | )] 68 | pub struct PlatformDoc; 69 | pub struct SecurityAddon; 70 | 71 | impl Modify for SecurityAddon { 72 | fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { 73 | let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered. 74 | components.add_security_scheme( 75 | "platform_token", 76 | SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), 77 | ); 78 | 79 | openapi.info.title = String::from("Satounki Platform API"); 80 | openapi.info.description = Option::from(String::from( 81 | "Used for managing company configuration via Terraform", 82 | )); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /api/src/auth/user_with_access_role.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use actix::fut::ready; 4 | use actix::fut::Ready; 5 | use actix_web::dev::Payload; 6 | use actix_web::web; 7 | use actix_web::FromRequest; 8 | use actix_web::HttpRequest; 9 | use color_eyre::eyre::anyhow; 10 | use database::Pool; 11 | 12 | use crate::auth::access_role::AccessRole; 13 | use crate::auth::authenticated::Authenticated; 14 | use crate::error; 15 | 16 | pub struct UserWithAccessRole { 17 | pub authenticated: Authenticated, 18 | phantom_data: std::marker::PhantomData, 19 | } 20 | 21 | impl Deref for UserWithAccessRole { 22 | type Target = Authenticated; 23 | 24 | fn deref(&self) -> &Self::Target { 25 | &self.authenticated 26 | } 27 | } 28 | 29 | impl FromRequest for UserWithAccessRole { 30 | type Error = actix_web::Error; 31 | type Future = Ready>; 32 | 33 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 34 | let mut connection = match req.app_data::>() { 35 | None => return ready(Err(error::Api::Other(anyhow!("database error")).into())), 36 | Some(pool) => match pool.get() { 37 | Ok(connection) => connection, 38 | Err(error) => return ready(Err(error::Api::DatabaseConnection(error).into())), 39 | }, 40 | }; 41 | 42 | let connection = &mut *connection; 43 | 44 | let company_access_role = R::as_enum(); 45 | 46 | match Authenticated::from_request(req, payload).into_inner() { 47 | Err(error) => ready(Err(error)), 48 | Ok(auth) => { 49 | let mut decision = 50 | ready(Err(error::Api::UnauthorizedUserCredentialsRequired.into())); 51 | 52 | if matches!( 53 | auth, 54 | Authenticated::Cookie { .. } | Authenticated::UserToken { .. } 55 | ) { 56 | if let Ok(user) = auth.user(connection) { 57 | if let Ok(roles) = user.roles(connection) { 58 | if roles.contains(&company_access_role) { 59 | decision = ready(Ok(Self { 60 | authenticated: auth, 61 | phantom_data: std::marker::PhantomData, 62 | })); 63 | } else { 64 | decision = ready(Err(error::Api::UnauthorizedAccessRoleRequired( 65 | R::as_str(), 66 | ) 67 | .into())); 68 | }; 69 | } 70 | } 71 | } 72 | 73 | decision 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /terraform-providers/satounkiplatform/internal/provider/company_model.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | satounki "satounki-platform" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | func (d companyResourceData) PostRequest() satounki.CompanyPostRequest { 12 | return satounki.CompanyPostRequest{ 13 | Domain: d.Domain.ValueString(), 14 | Name: d.Name.ValueString(), 15 | RootUserEmail: d.RootUserEmail.ValueString(), 16 | RootUserFirstName: d.RootUserFirstName.ValueStringPointer(), 17 | RootUserLastName: d.RootUserLastName.ValueStringPointer(), 18 | APIToken: d.ApiToken.ValueStringPointer(), 19 | WorkerKey: d.WorkerKey.ValueStringPointer(), 20 | } 21 | } 22 | 23 | func (d *companyResourceData) PostResponse(r satounki.CompanyPostResponse) { 24 | d.ID = types.StringValue(strconv.FormatInt(r.ID, 10)) 25 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 26 | d.Name = types.StringValue(r.Name) 27 | d.Domain = types.StringValue(r.Domain) 28 | d.RootUserEmail = types.StringValue(r.RootUserEmail) 29 | d.RootUserFirstName = types.StringValue(*r.RootUserFirstName) 30 | d.RootUserLastName = types.StringValue(*r.RootUserLastName) 31 | d.ApiToken = types.StringValue(*r.APIToken) 32 | d.WorkerKey = types.StringValue(*r.WorkerKey) 33 | } 34 | 35 | func (d companyResourceData) PutRequest() satounki.CompanyPutRequest { 36 | return satounki.CompanyPutRequest{ 37 | Domain: d.Domain.ValueString(), 38 | Name: d.Name.ValueString(), 39 | RootUserEmail: d.RootUserEmail.ValueString(), 40 | RootUserFirstName: d.RootUserFirstName.ValueStringPointer(), 41 | RootUserLastName: d.RootUserLastName.ValueStringPointer(), 42 | APIToken: d.ApiToken.ValueStringPointer(), 43 | WorkerKey: d.WorkerKey.ValueStringPointer(), 44 | } 45 | } 46 | 47 | func (d *companyResourceData) PutResponse(r satounki.CompanyPutResponse) { 48 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 49 | d.Name = types.StringValue(r.Name) 50 | d.Domain = types.StringValue(r.Domain) 51 | d.RootUserEmail = types.StringValue(r.RootUserEmail) 52 | d.RootUserFirstName = types.StringValue(*r.RootUserFirstName) 53 | d.RootUserLastName = types.StringValue(*r.RootUserLastName) 54 | d.ApiToken = types.StringValue(*r.APIToken) 55 | d.WorkerKey = types.StringValue(*r.WorkerKey) 56 | } 57 | 58 | func (d *companyResourceData) GetResponse(r satounki.CompanyGetResponse) { 59 | d.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) 60 | d.Name = types.StringValue(r.Name) 61 | d.Domain = types.StringValue(r.Domain) 62 | d.RootUserEmail = types.StringValue(r.RootUserEmail) 63 | d.RootUserFirstName = types.StringValue(*r.RootUserFirstName) 64 | d.RootUserLastName = types.StringValue(*r.RootUserLastName) 65 | d.ApiToken = types.StringValue(*r.APIToken) 66 | d.WorkerKey = types.StringValue(*r.WorkerKey) 67 | } 68 | --------------------------------------------------------------------------------