Skip to content

Commit

Permalink
Merge pull request #5143 from systeminit/jkeiser/eng-2880-add-simple-…
Browse files Browse the repository at this point in the history
…endpoint-to-generate-api-token-on-auth-api

feat: Add button to create an "automation token" and use it in SDF API
  • Loading branch information
jkeiser authored Dec 17, 2024
2 parents 7d3be6e + 5f45814 commit 34574ad
Show file tree
Hide file tree
Showing 35 changed files with 556 additions and 383 deletions.
23 changes: 22 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ lazy_static = "1.5.0"
manyhow = { version = "0.11.4", features = ["darling"] }
mime_guess = { version = "=2.0.4" } # TODO(fnichol): 2.0.5 sets an env var in build.rs which needs to be tracked, required by reqwest
miniz_oxide = { version = "0.8.0", features = ["simd"] }
monostate = "0.1.13"
names = { version = "0.14.0", default-features = false }
nix = { version = "0.26.0", features = [
"fs",
Expand Down
25 changes: 25 additions & 0 deletions app/auth-portal/src/pages/WorkspaceDetailsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@
DEFAULT
</div>
</template>
<IconButton
v-if="featureFlagsStore.AUTOMATION_API"
tooltip="Create API Token for Automation"
tooltipPlacement="top"
:icon="'clipboard-copy'"
size="lg"
class="flex-none"
iconTone="warning"
:iconIdleTone="draftWorkspace.isFavourite ? 'warning' : 'shade'"
iconBgActiveTone="action"
@click="createAutomationToken()"
/>
<IconButton
:tooltip="
draftWorkspace.isFavourite ? 'Remove Favourite' : 'Add Favourite'
Expand Down Expand Up @@ -254,10 +266,12 @@ import { useWorkspacesStore, WorkspaceId } from "@/store/workspaces.store";
import { tracker } from "@/lib/posthog";
import { API_HTTP_URL } from "@/store/api";
import MemberListItem from "@/components/MemberListItem.vue";
import { useFeatureFlagsStore } from "@/store/feature_flags.store";
const authStore = useAuthStore();
const workspacesStore = useWorkspacesStore();
const router = useRouter();
const featureFlagsStore = useFeatureFlagsStore();
const props = defineProps({
workspaceId: { type: String as PropType<WorkspaceId>, required: true },
Expand Down Expand Up @@ -428,6 +442,17 @@ const favouriteWorkspace = async (isFavourite: boolean) => {
draftWorkspace.isFavourite = isFavourite;
};
const createAutomationToken = async () => {
if (!props.workspaceId) return;
const { result } = await workspacesStore.CREATE_AUTOMATION_TOKEN(
props.workspaceId,
);
if (result.success) {
await navigator.clipboard.writeText(result.data.token);
}
};
const deleteWorkspace = async () => {
const res = await workspacesStore.DELETE_WORKSPACE(props.workspaceId);
if (res.result.success) {
Expand Down
2 changes: 2 additions & 0 deletions app/auth-portal/src/store/feature_flags.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export const useFeatureFlagsStore = () => {
ADMIN_PAGE: false,
ON_DEMAND_ASSETS: false,
CHANGE_USER_ROLE: false,
AUTOMATION_API: false,
}),
onActivated() {
posthog.onFeatureFlags((flags) => {
this.ADMIN_PAGE = flags.includes("auth_portal_admin_page");
this.ON_DEMAND_ASSETS = flags.includes("on_demand_assets");
this.CHANGE_USER_ROLE = flags.includes("change_user_role");
this.AUTOMATION_API = flags.includes("automation_api");
});
},
}),
Expand Down
7 changes: 7 additions & 0 deletions app/auth-portal/src/store/workspaces.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,13 @@ export const useWorkspacesStore = defineStore("workspaces", {
});
},

async CREATE_AUTOMATION_TOKEN(workspaceId: WorkspaceId) {
return new ApiRequest<{ token: string }>({
method: "post",
url: `/workspaces/${workspaceId}/createAutomationToken`,
});
},

async CHANGE_MEMBERSHIP(
workspaceId: WorkspaceId,
userId: UserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ const funcStore = useFuncStore();
const moduleStore = useModuleStore();

const selectedVariantId = computed(() => assetStore.selectedVariantId);

const selectedFuncId = computed(() => funcStore.selectedFuncId);
const loadAssetsRequestStatus = assetStore.getRequestStatus(
"LOAD_SCHEMA_VARIANT_LIST",
Expand Down
11 changes: 7 additions & 4 deletions bin/auth-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ instances.
While working on the auth stack, we still need to run it locally and configure things to point to our local auth stack:

- update auth-api env vars in `bin/auth-api/.env.local`
- if you don't have .env.local yet, copy the `auth api local dev .env.local` key into the `.env.local` file.
- fill in `AUTH0_CLIENT_SECRET` and `AUTH0_M2M_CLIENT_SECRET` and `STRIPE_API_KEY` (get from 1pass)
- (OPTIONAL) set auth-api redis url to a locally running redis instance (ex: `REDIS_URL=127.0.0.1:6379`) only if
needing to test redis. Falls back to in-memory storage...
Expand All @@ -44,12 +45,14 @@ While working on the auth stack, we still need to run it locally and configure t
VITE_AUTH_API_URL=http://localhost:9001
VITE_AUTH_PORTAL_URL=http://localhost:9000
```
- run the backend but using the local auth stack by setting env var `SI_AUTH_API_URL=http://localhost:9001` (
ex: `SI_AUTH_API_URL=http://localhost:9001 buck2 run dev:up`)
- run the db migrations (`pnpm run db:reset`) locally after booting your local database
- run the db migrations (`pnpm run db:reset`) locally after booting your local database (run `buck2 dev:stop` and then `buck2 dev` and hope nobody connects in the meantime!).
- run the auth api `pnpm run dev` in this directory or `pnpm dev:auth-api` at the root
- run the auth portal `pnpm run dev` in `app/auth-portal` or `pnpm dev:auth-portal` at the root
- (or run both by running `pnpm run dev:auth` at the root)
- (or run both by running `pnpm run dev:auth` at the root, but running them separately gives you nice console output)
- Run the backend, but pointing at your shiny new auth API:
```bash
SI_AUTH_API_URL=http://localhost:9001 SI_CREATE_WORKSPACE_PERMISSIONS=open buck2 run dev
```

## Deploy the Auth API to Production

Expand Down
6 changes: 5 additions & 1 deletion bin/auth-api/src/routes/auth.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ router.post("/auth/login", async (ctx) => {
throw new ApiError("Forbidden", "You do not have access to that workspace");
}

const token = createSdfAuthToken(user.id, workspaceId);
const token = createSdfAuthToken({
userId: user.id,
workspaceId,
role: "web",
});

ctx.body = { token };
});
Expand Down
80 changes: 39 additions & 41 deletions bin/auth-api/src/routes/workspace.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,21 @@ async function extractOwnWorkspaceIdParam(ctx: CustomRouteContext) {
return workspace;
}

async function isWorkspaceOwner(ctx: CustomRouteContext) {
async function authorizeWorkspaceRoute(ctx: CustomRouteContext, role?: RoleType) {
const workspace = await extractWorkspaceIdParam(ctx);

const authUser = extractAuthUser(ctx);
const memberRole = await userRoleForWorkspace(authUser.id, workspace.id);
if (memberRole !== RoleType.OWNER) {
throw new ApiError(
"Forbidden",
"You do not have the correct permisison to edit this workspace",
);

if (role) {
const memberRole = await userRoleForWorkspace(authUser.id, workspace.id);
if (memberRole !== role) {
throw new ApiError(
"Forbidden",
"You do not have the correct permission to edit this workspace",
);
}
}

return true;
return { authUser, workspace };
}

router.get("/workspaces/:workspaceId", async (ctx) => {
Expand Down Expand Up @@ -141,10 +143,7 @@ router.post("/workspaces/new", async (ctx) => {
});

router.patch("/workspaces/:workspaceId", async (ctx) => {
const authUser = extractAuthUser(ctx);
await isWorkspaceOwner(ctx);

const workspace = await extractOwnWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

const reqBody = validate(
ctx.request.body,
Expand Down Expand Up @@ -181,9 +180,7 @@ export type Member = {
signupAt: Date | null;
};
router.get("/workspace/:workspaceId/members", async (ctx) => {
extractAuthUser(ctx);

const workspace = await extractOwnWorkspaceIdParam(ctx);
const { workspace } = await authorizeWorkspaceRoute(ctx);

const members: Member[] = [];
const workspaceMembers = await getWorkspaceMembers(workspace.id);
Expand All @@ -202,11 +199,7 @@ router.get("/workspace/:workspaceId/members", async (ctx) => {
});

router.post("/workspace/:workspaceId/membership", async (ctx) => {
// user must be logged in
const authUser = extractAuthUser(ctx);
await isWorkspaceOwner(ctx);

const workspace = await extractOwnWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

const reqBody = validate(
ctx.request.body,
Expand Down Expand Up @@ -241,11 +234,7 @@ router.post("/workspace/:workspaceId/membership", async (ctx) => {
});

router.post("/workspace/:workspaceId/members", async (ctx) => {
// user must be logged in
const authUser = extractAuthUser(ctx);
await isWorkspaceOwner(ctx);

const workspace = await extractOwnWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

const reqBody = validate(
ctx.request.body,
Expand Down Expand Up @@ -273,11 +262,7 @@ router.post("/workspace/:workspaceId/members", async (ctx) => {
});

router.delete("/workspace/:workspaceId/members", async (ctx) => {
// user must be logged in
const authUser = extractAuthUser(ctx);
await isWorkspaceOwner(ctx);

const workspace = await extractOwnWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

const reqBody = validate(
ctx.request.body,
Expand Down Expand Up @@ -310,9 +295,7 @@ router.delete("/workspace/:workspaceId/members", async (ctx) => {
});

router.patch("/workspaces/:workspaceId/setDefault", async (ctx) => {
const authUser = extractAuthUser(ctx);

const workspace = await extractWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx);

tracker.trackEvent(authUser, "set_default_workspace", {
defaultWorkspaceSetBy: authUser.email,
Expand All @@ -327,9 +310,7 @@ router.patch("/workspaces/:workspaceId/setDefault", async (ctx) => {
});

router.patch("/workspaces/:workspaceId/favourite", async (ctx) => {
const authUser = extractAuthUser(ctx);

const workspace = await extractWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx);

const reqBody = validate(
ctx.request.body,
Expand Down Expand Up @@ -366,17 +347,16 @@ router.patch("/workspaces/:workspaceId/favourite", async (ctx) => {
});

router.get("/workspaces/:workspaceId/go", async (ctx) => {
const workspace = await extractOwnWorkspaceIdParam(ctx);
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx);

// TODO check this in all endpoints?
if (workspace.quarantinedAt !== null) {
throw new ApiError(
"Unauthorized",
`This workspace (ID ${workspace.id}) is quarantined. Contact SI support`,
);
}

const authUser = extractAuthUser(ctx);

// we require the user to have verified their email before they can log into a workspace
if (!authUser.emailVerified) {
// we'll first refresh from auth0 to make sure its actually not verified
Expand Down Expand Up @@ -424,6 +404,20 @@ router.get("/workspaces/:workspaceId/go", async (ctx) => {
ctx.redirect(redirectUrl);
});

router.post("/workspaces/:workspaceId/createAutomationToken", async (ctx) => {
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

const token = createSdfAuthToken({
userId: authUser.id,
workspaceId: workspace.id,
role: "automation",
}, {
expiresIn: "1 day",
});

ctx.body = { token };
});

router.post("/complete-auth-connect", async (ctx) => {
const reqBody = validate(
ctx.request.body,
Expand All @@ -441,7 +435,11 @@ router.post("/complete-auth-connect", async (ctx) => {
const user = await getUserById(connectPayload.userId);
if (!user) throw new ApiError("Conflict", "User no longer exists");

const token = createSdfAuthToken(user.id, workspace.id);
const token = createSdfAuthToken({
userId: user.id,
workspaceId: workspace.id,
role: "web",
});

ctx.body = {
user,
Expand Down
Loading

0 comments on commit 34574ad

Please sign in to comment.