Skip to content

Commit

Permalink
Merge pull request #1 from datum-cloud/authorization-webhook
Browse files Browse the repository at this point in the history
Authorization Webhook
  • Loading branch information
scotwells authored Jan 22, 2025
2 parents f7c06e3 + 6a0da6e commit 1a43111
Show file tree
Hide file tree
Showing 13 changed files with 1,041 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
"name": "Go",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm",
"features": {
"ghcr.io/devcontainers/features/common-utils": {
"installOhMyZsh": true,
"configureZshAsDefaultShell": true,
"installOhMyZshConfig": true,
"installZsh": true,
"upgradePackages": true
},
"ghcr.io/devcontainers/features/docker-in-docker": {},
"ghcr.io/dhoeric/features/act": {},
},
"customizations": {
"vscode": {
"extensions": [
"patbenatar.advanced-new-file",
"stkb.rewrap",
"github.vscode-github-actions",
"yzhang.markdown-all-in-one"
],
"settings": {
"rewrap.autoWrap.enabled": true
},
"editor.tabSize": 2
}
},

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",

// Configure tool-specific properties.
// "customizations": {},
}
65 changes: 65 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Build and Publish Docker Image

on:
push:
branches:
- main
pull_request:

jobs:
build-and-push:
permissions:
contents: read
packages: write
attestations: write
id-token: write

runs-on: ubuntu-latest

# Define the services that should be built.
strategy:
matrix:
service:
- datum-authorization-webhook

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Log in to GitHub Container Registry
uses: docker/[email protected]
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/[email protected]
with:
images: ghcr.io/datum-cloud/${{ matrix.service }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build ${{ matrix.service }}
id: push
uses: docker/[email protected]
with:
context: .
file: cmd/${{ matrix.service }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/datum-cloud/${{ matrix.service }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
21 changes: 21 additions & 0 deletions cmd/datum-authorization-webhook/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Use the official Go image as a build stage
FROM golang:1.23 AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy go.mod and go.sum files and download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy the rest of the application source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o datum-authorization-webhook ./cmd/datum-authorization-webhook

# Use a minimal image for the final container
FROM gcr.io/distroless/static
WORKDIR /app
COPY --from=builder /app/datum-authorization-webhook .
ENTRYPOINT ["/app/datum-authorization-webhook"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package iam

import (
"context"
"fmt"
"log/slog"
"slices"

"buf.build/gen/go/datum-cloud/iam/grpc/go/datum/iam/v1alpha/iamv1alphagrpc"
iampb "buf.build/gen/go/datum-cloud/iam/protocolbuffers/go/datum/iam/v1alpha"
"go.datumapis.com/datum/cmd/datum-authorization-webhook/app/internal/webhook"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"k8s.io/apiserver/pkg/authorization/authorizer"
)

var _ authorizer.Authorizer = &CoreControlPlaneAuthorizer{}

type CoreControlPlaneAuthorizer struct {
IAMClient iamv1alphagrpc.AccessCheckClient
}

// Authorize implements authorizer.Authorizer.
func (o *CoreControlPlaneAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
ctx, span := otel.Tracer("go.datum.net/k8s-authz-webhook").Start(ctx, "datum.k8s-authz-webhook.global.Authorize", trace.WithAttributes(
attribute.String("api_group", attributes.GetAPIGroup()),
attribute.String("resource_kind", attributes.GetResource()),
))
defer span.End()

if attributes.GetAPIGroup() != "resourcemanager.datumapis.com" {
slog.DebugContext(ctx, "No opinion on auth webhook request since API Group is not managed by webhook", slog.String("api_group", attributes.GetAPIGroup()))
return authorizer.DecisionNoOpinion, "", nil
}

var organizationID string
if orgIDs, set := attributes.GetUser().GetExtra()[webhook.OrganizationIDExtraKey]; !set {
return authorizer.DecisionDeny, "", fmt.Errorf("extra '%s' is required by core control plane authorizer", webhook.OrganizationIDExtraKey)
} else if len(orgIDs) > 1 {
return authorizer.DecisionDeny, "", fmt.Errorf("extra '%s' only supports one value, but multiple were provided: %v", webhook.OrganizationIDExtraKey, orgIDs)
} else {
organizationID = orgIDs[0]
}

req := getCheckAccessRequest(attributes, organizationID)

span.SetAttributes(
attribute.String("subject", req.Subject),
attribute.String("resource", req.Resource),
attribute.String("permission", req.Permission),
)

resp, err := o.IAMClient.CheckAccess(ctx, req)
if err != nil {
span.SetStatus(codes.Error, err.Error())
slog.ErrorContext(ctx, "failed to check subject access in IAM system", slog.String("error", err.Error()))
return authorizer.DecisionNoOpinion, "", err
}
span.SetAttributes(attribute.Bool("allowed", resp.GetAllowed()))

if resp.GetAllowed() {
slog.DebugContext(ctx, "subject was granted access through IAM service")
return authorizer.DecisionAllow, "", nil
}

return authorizer.DecisionDeny, "", nil
}

func getCheckAccessRequest(attributes authorizer.Attributes, organizationID string) *iampb.CheckAccessRequest {
req := &iampb.CheckAccessRequest{
Subject: "user:" + attributes.GetUser().GetName(),
Permission: fmt.Sprintf("%s/%s.%s", attributes.GetAPIGroup(), attributes.GetResource(), attributes.GetVerb()),
}

// Use the organization resource URL when acting on resource collections.
if slices.Contains([]string{"list", "create", "watch"}, attributes.GetVerb()) {
req.Resource = "resourcemanager.datumapis.com/organizations/" + organizationID
} else {
req.Resource = fmt.Sprintf("resourcemanager.datumapis.com/%s/%s", attributes.GetResource(), attributes.GetName())
req.Context = []*iampb.CheckContext{{
ContextType: &iampb.CheckContext_ParentRelationship{
ParentRelationship: &iampb.ParentRelationship{
ParentResource: "resourcemanager.datumapis.com/organizations/" + organizationID,
ChildResource: req.Resource,
},
},
}}
}

return req
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package iam

import (
"context"
"fmt"
"log/slog"

"buf.build/gen/go/datum-cloud/iam/grpc/go/datum/iam/v1alpha/iamv1alphagrpc"
iampb "buf.build/gen/go/datum-cloud/iam/protocolbuffers/go/datum/iam/v1alpha"
"go.datumapis.com/datum/cmd/datum-authorization-webhook/app/internal/webhook"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"k8s.io/apiserver/pkg/authorization/authorizer"
)

var _ authorizer.Authorizer = &ProjectControlPlaneAuthorizer{}

type ProjectControlPlaneAuthorizer struct {
IAMClient iamv1alphagrpc.AccessCheckClient
}

// Authorize implements authorizer.Authorizer.
func (o *ProjectControlPlaneAuthorizer) Authorize(
ctx context.Context, attributes authorizer.Attributes,
) (authorizer.Decision, string, error) {

ctx, span := otel.Tracer("go.datum.net/datum/cmd/datum-authorization-webhook").Start(ctx, "datum.authz-webhook.Authorize", trace.WithAttributes(
attribute.String("subject", attributes.GetUser().GetName()),
))
defer span.End()

var projectName string
if projectNames, set := attributes.GetUser().GetExtra()[webhook.ProjectExtraKey]; !set {
span.SetStatus(codes.Error, "no project ID present in webhook request")
return authorizer.DecisionDeny, "", fmt.Errorf("extra '%s' is required by core control plane authorizer", webhook.ProjectExtraKey)
} else if len(projectNames) > 1 {
span.SetStatus(codes.Error, "multiple project IDs present in webhook request")
return authorizer.DecisionDeny, "", fmt.Errorf("extra '%s' only supports one value, but multiple were provided: %v", webhook.ProjectExtraKey, projectNames)
} else {
projectName = projectNames[0]
}

resourceURL := "resourcemanager.datumapis.com/" + projectName
permissionName := fmt.Sprintf("%s/%s.%s", attributes.GetAPIGroup(), attributes.GetResource(), attributes.GetVerb())

span.SetAttributes(
attribute.String("resource", resourceURL),
attribute.String("permission", permissionName),
)

resp, err := o.IAMClient.CheckAccess(ctx, &iampb.CheckAccessRequest{
Resource: resourceURL,
Subject: "user:" + attributes.GetUser().GetName(),
Permission: permissionName,
})
if err != nil {
span.SetStatus(codes.Error, err.Error())
slog.ErrorContext(ctx, "failed to check subject access in IAM system", slog.String("error", err.Error()))
return authorizer.DecisionNoOpinion, "", err
}
span.SetAttributes(attribute.Bool("allowed", resp.GetAllowed()))

if resp.GetAllowed() {
slog.DebugContext(ctx, "subject was granted access through IAM service")
return authorizer.DecisionAllow, "", nil
}

return authorizer.DecisionDeny, "", nil
}
Loading

0 comments on commit 1a43111

Please sign in to comment.