Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Main to rebac #1036

Merged
merged 11 commits into from
Aug 28, 2023
22 changes: 7 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
# syntax=docker/dockerfile:1.3.1
FROM ubuntu:20.04 AS build
SHELL ["/bin/bash", "-c"]
ENV GVM_VERSION=master
COPY ./go.mod ./go.mod
RUN apt-get update && \
apt-get -y install gcc bison binutils make git gcc curl build-essential mercurial ca-certificates
RUN bash < <(curl -SL -v https://raw.githubusercontent.com/moovweb/gvm/${GVM_VERSION}/binscripts/gvm-installer) && \
source /root/.gvm/scripts/gvm && \
gvm install go$(cat go.mod | sed -n "/^go/p" | cut -d ' ' -f 2) -B && \
gvm use go$(cat go.mod | sed -n "/^go/p" | cut -d ' ' -f 2) --default


FROM build as build-env
FROM ubuntu:20.04 as build-env
ARG GIT_COMMIT
ARG VERSION
ARG GO_VERSION
WORKDIR /usr/src/jimm
SHELL ["/bin/bash", "-c"]
COPY . .
RUN apt update && apt install wget gcc -y
RUN wget -L "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz"
RUN tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
ENV PATH="${PATH}:/usr/local/go/bin"
RUN echo "${GIT_COMMIT}" | tee ./version/commit.txt
RUN echo "${VERSION}" | tee ./version/version.txt
RUN --mount=type=ssh source /root/.gvm/scripts/gvm && go mod vendor
RUN --mount=type=ssh source /root/.gvm/scripts/gvm && go build -tags version -o jimmsrv -v -a -mod vendor ./cmd/jimmsrv
RUN go build -tags version -o jimmsrv -v ./cmd/jimmsrv

# Define a smaller single process image for deployment
FROM ${DOCKER_REGISTRY}ubuntu:20.04 AS deploy-env
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ PROJECT := github.com/canonical/jimm

GIT_COMMIT := $(shell git rev-parse --verify HEAD)
GIT_VERSION := $(shell git describe --abbrev=0 --dirty)
GO_VERSION := $(shell go list -f {{.GoVersion}} -m)

ifeq ($(shell uname -p | sed -r 's/.*(x86|armel|armhf).*/golang/'), golang)
GO_C := golang
Expand Down Expand Up @@ -84,6 +85,7 @@ jimm-image:
docker build --target deploy-env \
--build-arg="GIT_COMMIT=$(GIT_COMMIT)" \
--build-arg="VERSION=$(GIT_VERSION)" \
--build-arg="GO_VERSION=$(GO_VERSION)" \
--tag jimm:latest .

jimm-snap:
Expand Down
4 changes: 4 additions & 0 deletions charms/jimm-k8s/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@ options:
description: |
Duration for the JWT expiry (defaults to 5 minutes).
default: 5m
macaroon-expiry-duration:
type: string
default: 24h
description: Expiry duration for authentication macaroons.
1 change: 1 addition & 0 deletions charms/jimm-k8s/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def _update_workload(self, event):
"PRIVATE_KEY": self.config.get("private-key", ""),
"PUBLIC_KEY": self.config.get("public-key", ""),
"JIMM_JWT_EXPIRY": self.config.get("jwt-expiry", "5m"),
"JIMM_MACAROON_EXPIRY_DURATION": self.config.get("macaroon-expiry-duration", "24h"),
}
if self._state.dsn:
config_values["JIMM_DSN"] = self._state.dsn
Expand Down
1 change: 1 addition & 0 deletions charms/jimm-k8s/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"PRIVATE_KEY": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=",
"PUBLIC_KEY": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=",
"JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": "0",
"JIMM_MACAROON_EXPIRY_DURATION": "24h",
}


Expand Down
1 change: 0 additions & 1 deletion charms/jimm/actions.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# Copyright 2021 Canonical Ltd
# See LICENSE file for licensing details.

4 changes: 4 additions & 0 deletions charms/jimm/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ options:
description: |
Duration for the JWT expiry (defaults to 5 minutes).
default: 5m
macaroon-expiry-duration:
type: string
default: 24h
description: Expiry duration for authentication macaroons.
1 change: 1 addition & 0 deletions charms/jimm/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def _on_config_changed(self, _):
"private_key": self.config.get("private-key"),
"audit_retention_period": self.config.get("audit-log-retention-period-in-days", ""),
"jwt_expiry": self.config.get("jwt-expiry", "5m"),
"macaroon_expiry_duration": self.config.get("macaroon-expiry-duration"),
}

if self.config.get("postgres-secret-storage", False):
Expand Down
3 changes: 2 additions & 1 deletion charms/jimm/templates/jimm.env
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ INSECURE_SECRET_STORAGE=enabled
{% endif %}
{%- if jwt_expiry %}
JIMM_JWT_EXPIRY={{jwt_expiry}}
{% endif %}
{% endif %}
JIMM_MACAROON_EXPIRY_DURATION={{macaroon_expiry_duration}}
22 changes: 15 additions & 7 deletions charms/jimm/tests/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,14 @@ def test_config_changed(self):
"private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=",
"audit-log-retention-period-in-days": "10",
"jwt-expiry": "10m",
"macaroon-expiry-duration": "48h",
}
)
self.assertTrue(os.path.exists(config_file))
with open(config_file) as f:
lines = f.readlines()
os.unlink(config_file)
self.assertEqual(len(lines), 19)
self.assertEqual(len(lines), 21)
self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=")
self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com")
self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1")
Expand All @@ -152,6 +153,7 @@ def test_config_changed(self):
lines[18].strip(),
"JIMM_JWT_EXPIRY=10m",
)
self.assertEqual(lines[20].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h")

def test_config_changed_redirect_to_dashboard(self):
config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env")
Expand All @@ -166,13 +168,14 @@ def test_config_changed_redirect_to_dashboard(self):
"public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=",
"private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=",
"audit-log-retention-period-in-days": "10",
"macaroon-expiry-duration": "48h",
}
)
self.assertTrue(os.path.exists(config_file))
with open(config_file) as f:
lines = f.readlines()
os.unlink(config_file)
self.assertEqual(len(lines), 19)
self.assertEqual(len(lines), 21)
self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=")
self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com")
self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1")
Expand All @@ -199,6 +202,7 @@ def test_config_changed_redirect_to_dashboard(self):
lines[18].strip(),
"JIMM_JWT_EXPIRY=5m",
)
self.assertEqual(lines[20].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h")

def test_config_changed_ready(self):
config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env")
Expand All @@ -212,13 +216,14 @@ def test_config_changed_ready(self):
"public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=",
"private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=",
"audit-log-retention-period-in-days": "10",
"macaroon-expiry-duration": "48h",
}
)
self.assertTrue(os.path.exists(config_file))
with open(config_file) as f:
lines = f.readlines()
os.unlink(config_file)
self.assertEqual(len(lines), 17)
self.assertEqual(len(lines), 19)
self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=")
self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com")
self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1")
Expand All @@ -244,6 +249,7 @@ def test_config_changed_ready(self):
lines[16].strip(),
"JIMM_JWT_EXPIRY=5m",
)
self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h")

def test_config_changed_with_agent(self):
config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env")
Expand All @@ -270,7 +276,7 @@ def test_config_changed_with_agent(self):

with open(config_file) as f:
lines = f.readlines()
self.assertEqual(len(lines), 17)
self.assertEqual(len(lines), 19)
self.assertEqual(
lines[0].strip(),
"BAKERY_AGENT_FILE=" + self.harness.charm._agent_filename,
Expand All @@ -280,6 +286,7 @@ def test_config_changed_with_agent(self):
self.assertEqual(lines[4].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models")
self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=info")
self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa")
self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=24h")

self.harness.charm._agent_filename = os.path.join(self.tempdir.name, "no-such-dir", "agent.json")
self.harness.update_config(
Expand All @@ -296,13 +303,14 @@ def test_config_changed_with_agent(self):
)
with open(config_file) as f:
lines = f.readlines()
self.assertEqual(len(lines), 17)
self.assertEqual(len(lines), 19)
self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=")
self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com")
self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1")
self.assertEqual(lines[4].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models")
self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=info")
self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa")
self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=24h")

def test_leader_elected(self):
leader_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm-leader.env")
Expand Down Expand Up @@ -583,14 +591,14 @@ def test_insecure_secret_storage(self):
with open(config_file) as f:
lines = f.readlines()
os.unlink(config_file)
self.assertEqual(len(lines), 19)
self.assertEqual(len(lines), 21)
self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 0)
self.harness.update_config({"postgres-secret-storage": True})
self.assertTrue(os.path.exists(config_file))
with open(config_file) as f:
lines = f.readlines()
os.unlink(config_file)
self.assertEqual(len(lines), 21)
self.assertEqual(len(lines), 23)
self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 1)


Expand Down
10 changes: 10 additions & 0 deletions cmd/jimmsrv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ func start(ctx context.Context, s *service.Service) error {
if addr == "" {
addr = ":http-alt"
}
macaroonExpiryDuration := 24 * time.Hour
durationString := os.Getenv("JIMM_MACAROON_EXPIRY_DURATION")
if durationString != "" {
expiry, err := time.ParseDuration(durationString)
if err != nil {
zapctx.Error(ctx, "failed to parse macaroon expiry duration", zap.Error(err))
}
macaroonExpiryDuration = expiry
}
jimmsvc, err := jimm.NewService(ctx, jimm.Params{
ControllerUUID: os.Getenv("JIMM_UUID"),
DSN: os.Getenv("JIMM_DSN"),
Expand All @@ -71,6 +80,7 @@ func start(ctx context.Context, s *service.Service) error {
PrivateKey: os.Getenv("BAKERY_PRIVATE_KEY"),
PublicKey: os.Getenv("BAKERY_PUBLIC_KEY"),
AuditLogRetentionPeriodInDays: os.Getenv("JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS"),
MacaroonExpiryDuration: macaroonExpiryDuration,
})
if err != nil {
return err
Expand Down
12 changes: 8 additions & 4 deletions internal/jimm/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,11 +528,15 @@ func (j *JIMM) doCloudAdmin(ctx context.Context, u *openfga.User, ct names.Cloud
// an unauthorized error.
return errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

if len(c.Regions) != 1 || len(c.Regions[0].Controllers) != 1 {
return errors.E(op, "cloud administration not available for %s", ct.Id())
// Ensure we always have at least 1 region for the cloud with at least 1 controller
// managing that region.
if len(c.Regions) < 1 || len(c.Regions[0].Controllers) < 1 {
zapctx.Error(ctx, "number of regions available in cloud", zap.String("cloud", c.Name), zap.Int("regions", len(c.Regions)))
if len(c.Regions) > 0 {
zapctx.Error(ctx, "number of controllers available for cloud/region", zap.Int("controllers", len(c.Regions[0].Controllers)))
}
return errors.E(op, fmt.Sprintf("cloud administration not available for %s", ct.Id()))
}

api, err := j.dial(ctx, &c.Regions[0].Controllers[0].Controller, names.ModelTag{})
if err != nil {
return errors.E(op, err)
Expand Down
15 changes: 15 additions & 0 deletions internal/jimm/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,7 @@ const grantCloudAccessTestEnv = `clouds:
host-cloud-region: test-cloud/test-cloud-region
regions:
- name: default
- name: region2
users:
- user: alice@external
access: admin
Expand All @@ -1250,6 +1251,9 @@ controllers:
- cloud: test
region: default
priority: 1
- cloud: test
region: region2
priority: 1
`

var grantCloudAccessTests = []struct {
Expand Down Expand Up @@ -1306,6 +1310,17 @@ var grantCloudAccessTests = []struct {
},
Priority: 1,
}},
}, {
Name: "region2",
Controllers: []dbmodel.CloudRegionControllerPriority{{
Controller: dbmodel.Controller{
Name: "controller-1",
UUID: "00000001-0000-0000-0000-000000000001",
CloudName: "test-cloud",
CloudRegion: "test-cloud-region",
},
Priority: 1,
}},
}},
Users: []dbmodel.UserCloudAccess{{
Username: "alice@external",
Expand Down
17 changes: 14 additions & 3 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ type Params struct {
// auditLogRetentionPeriodInDays is the number of days detailing how long
// to keep an audit log for before purging it from the database.
AuditLogRetentionPeriodInDays string

// MacaroonExpiryDuration holds the expiry duration of authentication macaroons.
MacaroonExpiryDuration time.Duration
}

// A Service is the implementation of a JIMM server.
Expand Down Expand Up @@ -442,11 +445,19 @@ func newAuthenticator(ctx context.Context, db *db.Database, client *openfga.OFGA
if err != nil {
return nil, err
}

if p.MacaroonExpiryDuration == 0 {
p.MacaroonExpiryDuration = 24 * time.Hour
}

return auth.JujuAuthenticator{
Bakery: identchecker.NewBakery(identchecker.BakeryParams{
RootKeyStore: dbrootkeystore.NewRootKeys(100, nil).NewStore(db, dbrootkeystore.Policy{
ExpiryDuration: 24 * time.Hour,
}),
RootKeyStore: dbrootkeystore.NewRootKeys(100, nil).NewStore(
db,
dbrootkeystore.Policy{
ExpiryDuration: p.MacaroonExpiryDuration,
},
),
Locator: httpbakery.NewThirdPartyLocator(nil, tps),
Key: key,
IdentityClient: candidClient,
Expand Down
3 changes: 2 additions & 1 deletion snaps/jimmctl/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ apps:
- dot-local-share-juju
- network


parts:
jimmctl:
plugin: go
Expand All @@ -32,7 +33,7 @@ parts:

files:
plugin: dump
source: snaps/jimmctl/files
source: snaps/jimmctl/files/
source-type: local
organize:
jimmctl.wrapper: bin/wrappers/jimmctl
Expand Down