Skip to content

Commit 023b5d8

Browse files
feat(groups): add ServiceAccountAccessToken managed resource (#326)
* feat(groups): add ServiceAccountAccessToken managed resource Adds a group-scoped ServiceAccountAccessToken managed resource that manages the personal access token of a group service account. Owner mode (default): the ProviderConfig is a group owner and the token is managed via the service-account endpoints: - Create -> Groups.CreateServiceAccountPersonalAccessToken - Observe -> Groups.ListServiceAccountPersonalAccessTokens (match by token id) - Rotate -> Groups.RotateServiceAccountPersonalAccessToken - Revoke -> Groups.RevokeServiceAccountPersonalAccessToken Self-managed mode: when the referenced ProviderConfig authenticates with the very token this resource writes to its connection secret (detected when the PersonalAccessToken credential secretRef matches writeConnectionSecretToRef by namespace, name and key), the provider acts as the service account itself and uses the self endpoints instead: - Observe -> GET /personal_access_tokens/self (self-inform; external name is auto-adopted from the response) - Rotate -> RotatePersonalAccessTokenSelf - Revoke -> RevokePersonalAccessTokenSelf This enables a self-sustaining loop of short-lived, self-rotating tokens used to reconcile a group. A dead self-token surfaces as a clear terminal error (reseed the credentials secret). A SelfManaged status condition reports the detected mode. The external name is the token id and the token value is written to the connection secret on create/rotate. Rotation, expiresAt/renewalPeriodDays and renewBeforeDays semantics match the group AccessToken controller. groupId, serviceAccountId, name and scopes are immutable (enforced via CEL); the rotation-timing fields stay mutable. Fixes #324 Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com> * fix(groups): address ServiceAccountAccessToken review feedback - self mode: validate the adopted PAT belongs to spec.forProvider.serviceAccountId (PersonalAccessToken.UserID) and surface a terminal error on mismatch, so a miswired credentials secret can no longer rotate/revoke the wrong service account's token while the reconcile still succeeds. - owner mode: narrow the rotate->create fallback. Only fall through to a fresh create when the existing token is genuinely gone (404, or the specific 'Token already revoked' 400). Any other error (generic 400, 403, 5xx, transport) now returns instead of minting a second token. - cluster scope: fix self-mode detection for cluster-scoped resources. A cluster resource has no namespace and writes to a full SecretReference, so isSelfManaged must compare the credential secret namespace against the write reference's namespace. Implemented as a generator replacement so the cluster zz_ code is regenerated correctly. - client: filter the service-account token list with state=active to avoid paginating through revoked/expired tokens on every Observe poll. - crd: add the 'saat' shortName for friendlier kubectl usage. Regenerated cluster-scoped code and CRDs via make generate. Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com> * fix(common): carry CredentialsSecretRef through the legacy ProviderConfig path Cluster-scoped resources are LegacyManaged and resolve their credentials via UseLegacyProviderConfig, which built a Config without CredentialsSecretRef. As a result isSelfManaged() always saw a nil ref and self-managed mode never activated for cluster-scoped ServiceAccountAccessTokens, even when the ProviderConfig read from the exact secret the resource writes to (it stayed SelfManaged=False / OwnerManaged and then 404'd on the owner path). Populate CredentialsSecretRef from pc.Spec.Credentials.SecretRef in the legacy path, mirroring the modern namespaced path, so self-mode detection works for cluster-scoped resources too. Co-authored fix and test verifying the legacy builder carries the ref. Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com> * style(common): fix gci import grouping in helper_test.go Move the provider-gitlab-local imports into their own trailing gci group to satisfy the project's gci section order (standard, default, prefix). Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com> --------- Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com>
1 parent 0e1dbef commit 023b5d8

31 files changed

Lines changed: 5725 additions & 8 deletions

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,51 @@ spec:
4444
kubectl apply -f examples/providerconfig/provider.yaml
4545
```
4646

47+
### Self-rotating service account tokens
48+
49+
The namespaced `groups.gitlab.m.crossplane.io/v1alpha1` `ServiceAccountAccessToken`
50+
resource can keep a short-lived token alive by rotating it before it expires.
51+
52+
It supports two modes, selected automatically:
53+
54+
* **Owner mode** (default): the `ProviderConfig` is a group owner. The token is
55+
created, observed, rotated and revoked through the group service-account
56+
endpoints.
57+
* **Self-managed mode**: the `ProviderConfig` authenticates with the *very token*
58+
this resource manages — i.e. the `ProviderConfig.credentials.secretRef` points
59+
at the same secret (namespace, name and `token` key) that the resource writes
60+
via `writeConnectionSecretToRef`. The provider then acts as the service account
61+
itself and uses the self endpoints (`GET/POST /personal_access_tokens/self`).
62+
This lets a short-lived token reconcile a whole group and keep itself alive by
63+
self-rotating. A `SelfManaged` status condition reports the active mode.
64+
65+
> **Bootstrap secret type.** In self-managed mode the rotated token is written
66+
> back into the secret the `ProviderConfig` reads. Crossplane only writes
67+
> connection secrets it controls, so a **hand-created** bootstrap secret must use
68+
> the connection secret type `connection.crossplane.io/v1alpha1` — a default
69+
> `Opaque` secret (e.g. from `kubectl create secret generic`, which has no
70+
> `--type` flag) is rejected with `refusing to modify uncontrolled secret of type
71+
> "Opaque"`. Create it from a manifest:
72+
>
73+
> ```yaml
74+
> apiVersion: v1
75+
> kind: Secret
76+
> metadata:
77+
> name: gitlab-self-rotating-token
78+
> namespace: default
79+
> type: connection.crossplane.io/v1alpha1
80+
> stringData:
81+
> # a service account PAT with at least the `api` and `self_rotate` scopes
82+
> token: "<PERSONAL_ACCESS_TOKEN>"
83+
> ```
84+
>
85+
> Alternatively, bootstrap in owner mode first (Crossplane then creates and owns
86+
> a correctly typed connection secret), and switch `providerConfigRef` to the
87+
> self `ProviderConfig` afterwards.
88+
89+
See [`examples/groups/serviceaccountaccesstoken.yaml`](examples/groups/serviceaccountaccesstoken.yaml)
90+
for full examples of both modes.
91+
4792
## Contributing
4893
4994
provider-gitlab is a community driven project and we welcome contributions. See

apis/cluster/groups/v1alpha1/zz_generated.deepcopy.go

Lines changed: 198 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/cluster/groups/v1alpha1/zz_generated.managed.go

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/cluster/groups/v1alpha1/zz_generated.managedlist.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/cluster/groups/v1alpha1/zz_referencers.go

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)