Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ relevant section below for more details.
- `ORGANISATION_NAME`: Organisation name to use for the default organisation.
- `PROJECT_NAME`: Project name to use for the default project.
- `ENABLE_GZIP_COMPRESSION`: If Django should gzip compress HTTP responses. Defaults to `False`.
- `TRUST_RELATIONSHIP_ACCESS_TOKEN_LIFETIME_SECONDS`: Lifetime of access tokens minted by the OIDC trust relationship
token exchange. Defaults to `3600`.
- `OIDC_TOKEN_EXCHANGE_THROTTLE_RATE`: Rate limit for the OIDC token exchange endpoint. Defaults to `60/min`.
- `GOOGLE_ANALYTICS_KEY`: If Google Analytics is required, add your tracking code.
- `GOOGLE_SERVICE_ACCOUNT`: Service account JSON for accessing the Google API, used for getting usage of an
organisation - needs access to analytics.readonly scope.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,107 @@ title: Authentication
sidebar_label: Authentication
---

To interact with the Admin API, you need to authenticate your requests using an API Token associated with your Organisation.
To interact with the Admin API, you need to authenticate your requests using an API Token associated with your
Organisation, or a short-lived access token obtained through an [OIDC trust relationship](#oidc-trust-relationships).

:::info

Granular permissions for your API tokens and OIDC trust relationships require an
[Enterprise or Scale-Up subscription](https://flagsmith.com/pricing).

:::

## Generating an API Token

You can generate an API Token from the **Organisation Settings** page in the Flagsmith dashboard.

1. Click on your Organisation name in the top navigation panel.
2. Go to the **API Keys** tab.
3. Click **Create API Key**.
1. Click on your Organisation name in the top navigation panel.
2. Go to the **API Access** tab.
3. Click **Create API Key**.

Give your key a descriptive name so you can remember what it's used for.

## Using the API Token

Once you have your token, you need to include it in your API requests as an `Authorization` header. The token should be prefixed with `Api-Key`.
Once you have your token, you need to include it in your API requests as an `Authorization` header. The token should be
prefixed with `Api-Key`.

```bash
Authorization: Api-Key <API TOKEN FROM ORGANISATION PAGE>
```

This token grants access to manage all projects within that organisation, so be sure to keep it secure and never expose it in client-side applications.
An API token acts with the permissions selected when it was created: either full organisation admin — able to manage all
projects within that organisation — or a set of RBAC roles. Be sure to keep it secure and never expose it in client-side
applications.

For SaaS customers, the base URL for the Admin API is `https://api.flagsmith.com/`. If you are self-hosting, you will
need to use your own API URL.

## OIDC trust relationships

Trust relationships let a workload that already has an OIDC identity — for example, a GitHub Actions job — call the
Admin API without any stored secrets. The workload exchanges its OIDC token for a short-lived Flagsmith access token, in
the same way cloud providers implement workload identity federation.

### Configuring a trust relationship

1. Click on your Organisation name in the top navigation panel.
2. Go to the **API Access** tab.
3. Under **Trust relationships**, click **Add trust relationship** and pick a provider.

#### GitHub Actions

The GitHub Actions form asks for a repository and, optionally, a GitHub environment and a workflow filename:

- **Repository**: with the Flagsmith GitHub integration installed, pick the repository from a list — the trust
relationship then pins the repository's ID. Without the integration, type the owner and name.
- **GitHub environment**: if set, tokens must carry the matching `environment` claim, so the workflow job must run in
that GitHub environment.
- **Workflow filename**: if set, tokens must come from that workflow file — matched via the `workflow_ref` claim —
regardless of branch or tag.
- **Is admin / roles**: the permissions granted to exchanged tokens — either full organisation admin, or a set of RBAC
roles, exactly as with Master API Keys.

The form shows a ready-made workflow snippet reflecting your configuration.

#### Other OIDC providers

The freeform option supports any OIDC identity provider, such as GitLab CI or Kubernetes:

- **Trusted issuer URL**: the OIDC issuer. The issuer must serve OIDC discovery metadata over HTTPS.
- **Expected audience**: the `aud` claim the token must carry. Each issuer and audience pair must be unique, so use a
distinct audience per trust relationship.
- **Claim matching rules**: additional claims the token must match. Values support `*` wildcards, and a rule matches if
any of its values match. All rules must match.
- **Is admin / roles**: as above.

### Exchanging a token

`POST /api/v1/auth/oidc/token/` with the OIDC token in the request body:

```bash
curl -X POST 'https://api.flagsmith.com/api/v1/auth/oidc/token/' \
-H 'Content-Type: application/json' \
-d '{"token": "<OIDC TOKEN>"}'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

If you are self-hosting, replace `https://api.flagsmith.com` with your own API URL.

A successful exchange returns a short-lived access token:

```json
{
"access_token": "<ACCESS TOKEN>",
"token_type": "Bearer",
"expires_in": 3600
}
```

Use it as a bearer token on Admin API requests:

```bash
Authorization: Bearer <ACCESS TOKEN>
```

For SaaS customers, the base URL for the Admin API is `https://api.flagsmith.com/`. If you are self-hosting, you will need to use your own API URL.
Access tokens expire after one hour by default. Deleting a trust relationship, or changing its roles, applies to
already-issued tokens immediately. Exchanged tokens cannot manage API keys or trust relationships.
4 changes: 2 additions & 2 deletions docs/docs/managing-flags/code-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
This workflow needs the following added to [_Settings > Secrets and variables > Actions_](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) in GitHub:

- `FLAGSMITH_PROJECT_ID` (variable): obtain from the Flagsmith dashboard URL, e.g. `/project/<id>/...`
- `FLAGSMITH_CODE_REFERENCES_API_KEY` (secret): obtain from _Organisation Settings > API Keys_ in Flagsmith
- `FLAGSMITH_CODE_REFERENCES_API_KEY` (secret): obtain from _Organisation Settings > API Access_ in Flagsmith

### Advanced configuration

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
This workflow needs the following added to [_Settings > Secrets and variables > Actions_](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) in GitHub:

- `FLAGSMITH_PROJECT_ID` (variable): obtain from the Flagsmith dashboard URL, e.g. `/project/<id>/...`
- `FLAGSMITH_CODE_REFERENCES_API_KEY` (secret): obtain from _Organisation Settings > API Keys_ in Flagsmith
- `FLAGSMITH_CODE_REFERENCES_API_KEY` (secret): obtain from _Organisation Settings > API Access_ in Flagsmith

---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/third-party-integrations/backstage.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ proxy:
Authorization: Api-Key FLAGSMITH_API_TOKEN
```

- `FLAGSMITH_API_TOKEN`: obtain from Organisation Settings > API Keys in Flagsmith.
- `FLAGSMITH_API_TOKEN`: obtain from Organisation Settings > API Access in Flagsmith.
- If you are self-hosting Flagsmith, replace the `target` URL with your own Flagsmith API address, e.g.
`https://flagsmith.example.com/api/v1`.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/third-party-integrations/ci-cd/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from your account settings page which will help you access these variables.
### Organisation API Key

In order to configure the Flagsmith Terraform provider we need an API key. To generate one, head over to the
Organisation Settings page (click `Organisation` at the top of the page), then `API Keys`, then `Create API Key`.
Organisation Settings page (click `Organisation` at the top of the page), then `API Access`, then `Create API Key`.

:::info

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { trustRelationshipErrorMessage } from 'components/pages/organisation-set
import useTrustRelationshipRoles from 'components/pages/organisation-settings/tabs/trust-relationships/hooks/useTrustRelationshipRoles'
import TrustRelationshipPermissionsFields from 'components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipPermissionsFields'
import WorkflowSetupSnippet from 'components/pages/organisation-settings/tabs/trust-relationships/WorkflowSetupSnippet'
import { GITHUB_ISSUER } from 'components/pages/organisation-settings/tabs/trust-relationships/github'
import {
GITHUB_ISSUER,
githubWorkflowRefPattern,
parseGithubWorkflowFilename,
} from 'components/pages/organisation-settings/tabs/trust-relationships/github'

type GithubTrustRelationshipFormProps = {
organisationId: number
Expand Down Expand Up @@ -61,6 +65,10 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
const [environment, setEnvironment] = useState(
ruleValue(trustRelationship, 'environment') || '',
)
const [workflowFilename, setWorkflowFilename] = useState(
parseGithubWorkflowFilename(ruleValue(trustRelationship, 'workflow_ref')) ||
'',
)
const [isAdmin, setIsAdmin] = useState(trustRelationship?.is_admin ?? true)
const { addRole, assignRoles, clearRoles, removeRole, roles } =
useTrustRelationshipRoles(organisationId, trustRelationship)
Expand Down Expand Up @@ -148,6 +156,12 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
if (environment.trim()) {
claimRules.push({ claim: 'environment', values: [environment.trim()] })
}
if (workflowFilename.trim()) {
claimRules.push({
claim: 'workflow_ref',
values: [githubWorkflowRefPattern(workflowFilename.trim())],
})
}
const body = {
audience,
claim_rules: claimRules,
Expand Down Expand Up @@ -277,6 +291,16 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
}
placeholder='e.g. production'
/>
<InputGroup
title='Workflow filename (optional)'
tooltip='If set, only this workflow file can exchange tokens, on any branch or tag.'
inputProps={{ className: 'full-width' }}
value={workflowFilename}
onChange={(e: InputEvent) =>
setWorkflowFilename(Utils.safeParseEventValue(e))
}
placeholder='e.g. deploy.yml'
/>
{!!audience && (
<>
<InputGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import React, { FC, useMemo } from 'react'
import CodeCard from 'components/pages/onboarding/OnboardingConnectPanel/CodeCard'
import Icon from 'components/icons/Icon'
import Project from 'common/project'
import { isDefaultGithubAudience } from 'components/pages/organisation-settings/tabs/trust-relationships/github'

const SAAS_API_HOST = 'api.flagsmith.com'

// The CLI defaults to the SaaS API, so the snippet only needs an explicit
// api-url on other instances. It takes the base URL without /api/v1, which
// the CLI appends itself.
export const getNonDefaultApiUrl = (): string | undefined => {
// Project.api can be relative, e.g. /api/v1/
const resolved = new URL(Project.api, window.location.origin)
if (resolved.host === SAAS_API_HOST) {
return undefined
}
return resolved.href.replace(/\/api\/v1\/?$/, '')
}

type WorkflowSetupSnippetProps = {
audience: string
environment?: string
Expand All @@ -24,8 +39,19 @@ const WorkflowSetupSnippet: FC<WorkflowSetupSnippetProps> = ({
' steps:',
' - uses: Flagsmith/setup-cli@v1',
)
const withInputs: string[] = []
if (!isDefaultAudience) {
lines.push(' with:', ` audience: ${audience}`)
withInputs.push(`audience: ${audience}`)
}
const apiUrl = getNonDefaultApiUrl()
if (apiUrl) {
withInputs.push(`api-url: ${apiUrl}`)
}
if (withInputs.length) {
lines.push(
' with:',
...withInputs.map((input) => ` ${input}`),
)
}
return lines.join('\n')
}, [environment, isDefaultAudience, audience])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
GITHUB_ISSUER,
githubWorkflowRefPattern,
isGithubFormEditable,
parseGithubWorkflowFilename,
} from 'components/pages/organisation-settings/tabs/trust-relationships/github'
import {
TrustRelationship,
Expand Down Expand Up @@ -28,13 +30,23 @@ const ENVIRONMENT: TrustRelationshipClaimRule = {
claim: 'environment',
values: ['production'],
}
const WORKFLOW_REF: TrustRelationshipClaimRule = {
claim: 'workflow_ref',
values: [githubWorkflowRefPattern('deploy.yml')],
}
const FOREIGN_WORKFLOW_REF: TrustRelationshipClaimRule = {
claim: 'workflow_ref',
values: ['Flagsmith/flagsmith/.github/workflows/deploy.yml@refs/heads/main'],
}

describe('isGithubFormEditable', () => {
it.each`
description | claimRules | expected
${'a single repository rule'} | ${[REPOSITORY]} | ${true}
${'a repository and environment'} | ${[REPOSITORY, ENVIRONMENT]} | ${true}
${'a repository pinned by id'} | ${[REPOSITORY_ID]} | ${true}
${'a workflow filename rule'} | ${[REPOSITORY, WORKFLOW_REF]} | ${true}
${'a foreign workflow_ref rule'} | ${[REPOSITORY, FOREIGN_WORKFLOW_REF]} | ${false}
${'no repository selector'} | ${[ENVIRONMENT]} | ${false}
${'both repository selectors'} | ${[REPOSITORY, REPOSITORY_ID]} | ${false}
${'a duplicated claim'} | ${[REPOSITORY, ENVIRONMENT, ENVIRONMENT]} | ${false}
Expand All @@ -53,3 +65,23 @@ describe('isGithubFormEditable', () => {
expect(isGithubFormEditable(relationship)).toBe(false)
})
})

describe('parseGithubWorkflowFilename', () => {
it('round-trips a pattern written by the form', () => {
// Given
const pattern = githubWorkflowRefPattern('deploy.yml')

// When / Then
expect(parseGithubWorkflowFilename(pattern)).toBe('deploy.yml')
})

it.each`
description | workflowRef
${'an undefined value'} | ${undefined}
${'a literal claim'} | ${'a/b/.github/workflows/deploy.yml@refs/heads/main'}
${'an unrelated pattern'} | ${'*/deploy.yml@*'}
`('returns undefined for $description', ({ workflowRef }) => {
// Given / When / Then
expect(parseGithubWorkflowFilename(workflowRef)).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,25 @@ import { TrustRelationship } from 'common/types/responses'
export const GITHUB_ISSUER = 'https://token.actions.githubusercontent.com'

// Claims the GitHub form can round-trip; anything else edits as freeform.
const GITHUB_FORM_CLAIMS = ['repository', 'repository_id', 'environment']
const GITHUB_FORM_CLAIMS = [
'repository',
'repository_id',
'environment',
'workflow_ref',
]

// The repository rule already pins the repository, so the workflow rule only
// needs to enforce the path — a wildcard prefix survives repository renames,
// and the wildcard ref leaves branch filtering to the environment rule.
export const githubWorkflowRefPattern = (filename: string): string =>
`*/.github/workflows/${filename}@*`
Comment on lines +16 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject wildcard syntax in workflow filenames.

Line 17 inserts filename directly into a wildcard claim rule. If the value is *, the saved rule permits every workflow under .github/workflows/, not one workflow file. This can widen token-exchange access despite the form field describing a single filename.

Accept only literal workflow filenames, or escape all claim-matcher metacharacters before creating the pattern. Add cases for *, ?, brackets, path separators, and @.


const GITHUB_WORKFLOW_REF_REGEX = /^\*\/\.github\/workflows\/(.+)@\*$/

export const parseGithubWorkflowFilename = (
workflowRef: string | undefined,
): string | undefined =>
workflowRef ? GITHUB_WORKFLOW_REF_REGEX.exec(workflowRef)?.[1] : undefined

export const isGithubFormEditable = (
trustRelationship: TrustRelationship,
Expand All @@ -21,7 +39,10 @@ export const isGithubFormEditable = (
repositorySelectors.length === 1 &&
trustRelationship.claim_rules.every(
(rule) =>
GITHUB_FORM_CLAIMS.includes(rule.claim) && rule.values.length === 1,
GITHUB_FORM_CLAIMS.includes(rule.claim) &&
rule.values.length === 1 &&
(rule.claim !== 'workflow_ref' ||
!!parseGithubWorkflowFilename(rule.values[0])),
)
)
}
Expand Down
Loading