forked from baron/baron-sso
Merge commit '85998bd82ecc8870af3c260f4a6f251ea1d12231' into featur/tenantsign
This commit is contained in:
@@ -111,12 +111,12 @@ HYDRA_ADMIN_URL=http://hydra:4445
|
|||||||
HYDRA_PUBLIC_URL=${OATHKEEPER_PUBLIC_URL}/oidc
|
HYDRA_PUBLIC_URL=${OATHKEEPER_PUBLIC_URL}/oidc
|
||||||
|
|
||||||
# OIDC 클라이언트 callback (콤마 구분)
|
# OIDC 클라이언트 callback (콤마 구분)
|
||||||
ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback
|
ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback,https://sso.hmac.kr/auth/callback
|
||||||
DEVFRONT_CALLBACK_URLS=http://localhost:5174/callback
|
DEVFRONT_CALLBACK_URLS=http://localhost:5174/callback,https://sso.hmac.kr/devfront/callback
|
||||||
|
|
||||||
# Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택)
|
# Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택)
|
||||||
# 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다.
|
# 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다.
|
||||||
KRATOS_ALLOWED_RETURN_URLS_EXTRA=
|
KRATOS_ALLOWED_RETURN_URLS_EXTRA=[]
|
||||||
|
|
||||||
# Oathkeeper JWKS (내부 통신용)
|
# Oathkeeper JWKS (내부 통신용)
|
||||||
JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
|
JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
|
||||||
|
|||||||
@@ -1,30 +1,16 @@
|
|||||||
name: Code Check
|
name: Code Check
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
run_lint:
|
|
||||||
description: "Run linters for Go and Flutter"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
run_backend_tests:
|
|
||||||
description: "Run backend Go tests"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
run_userfront_tests:
|
|
||||||
description: "Run userfront Flutter tests"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_lint == true }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -68,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
backend-tests:
|
backend-tests:
|
||||||
needs: lint
|
needs: lint
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true }}
|
if: ${{ always() }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
@@ -102,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
userfront-tests:
|
userfront-tests:
|
||||||
needs: lint
|
needs: lint
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_userfront_tests == true }}
|
if: ${{ always() }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -12,11 +12,11 @@ COMPOSE_ORY := compose.ory.yaml
|
|||||||
COMPOSE_APP := docker-compose.yaml
|
COMPOSE_APP := docker-compose.yaml
|
||||||
AUTH_CONFIG_ENV := .generated/auth-config.env
|
AUTH_CONFIG_ENV := .generated/auth-config.env
|
||||||
|
|
||||||
COMPOSE_ENV_FILES :=
|
COMPOSE_CLI_ENV_ARGS :=
|
||||||
ifneq (,$(wildcard ./.env))
|
ifneq (,$(wildcard ./.env))
|
||||||
COMPOSE_ENV_FILES += --env-file .env
|
COMPOSE_CLI_ENV_ARGS += --env-file .env
|
||||||
endif
|
endif
|
||||||
COMPOSE_ENV_FILES += --env-file $(AUTH_CONFIG_ENV)
|
COMPOSE_CLI_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV)
|
||||||
|
|
||||||
# --- 인증 설정 빌드/검증 ---
|
# --- 인증 설정 빌드/검증 ---
|
||||||
build-auth-config:
|
build-auth-config:
|
||||||
@@ -36,7 +36,7 @@ verify-auth-config: validate-auth-config
|
|||||||
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
|
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
|
||||||
up-all: validate-auth-config
|
up-all: validate-auth-config
|
||||||
@echo "Starting ALL stacks (infra + ory + app)..."
|
@echo "Starting ALL stacks (infra + ory + app)..."
|
||||||
docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d
|
||||||
|
|
||||||
# --- 개별 스택 실행 ---
|
# --- 개별 스택 실행 ---
|
||||||
up-infra:
|
up-infra:
|
||||||
@@ -45,15 +45,15 @@ up-infra:
|
|||||||
|
|
||||||
up-ory: validate-auth-config
|
up-ory: validate-auth-config
|
||||||
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
|
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
|
||||||
docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_ORY) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d
|
||||||
|
|
||||||
up-app: validate-auth-config
|
up-app: validate-auth-config
|
||||||
@echo "Starting App stack (backend/userfront/adminfront/devfront)..."
|
@echo "Starting App stack (backend/userfront/adminfront/devfront)..."
|
||||||
docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_APP) up -d
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d
|
||||||
|
|
||||||
up-backend: validate-auth-config
|
up-backend: validate-auth-config
|
||||||
@echo "Starting Backend only..."
|
@echo "Starting Backend only..."
|
||||||
docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_APP) up -d backend
|
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d backend
|
||||||
|
|
||||||
up-dev: up-infra up-ory
|
up-dev: up-infra up-ory
|
||||||
@echo "Dev stack is up (infra + ory)."
|
@echo "Dev stack is up (infra + ory)."
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -155,13 +155,43 @@ Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. 비
|
|||||||
```bash
|
```bash
|
||||||
cp .env.sample .env
|
cp .env.sample .env
|
||||||
```
|
```
|
||||||
2. **IDP 우선순위와 Ory 엔드포인트를 지정**합니다. 기본값은 Ory 입니다
|
2. `.env`를 작성합니다. (아래 작성 규칙 필수)
|
||||||
```
|
|
||||||
IDP_PROVIDER=ory
|
### `.env` 작성 규칙 (중요)
|
||||||
KRATOS_ADMIN_URL=http://kratos:4434
|
- `KEY=value` 한 줄만 사용하고, **값 뒤에 같은 줄 주석을 붙이지 않습니다.**
|
||||||
HYDRA_ADMIN_URL=http://hydra:4445
|
- 주석이 필요하면 반드시 **윗줄에 별도 주석 라인**으로 작성합니다.
|
||||||
HYDRA_PUBLIC_URL=http://hydra:4444
|
- URL 값 끝에 공백이 들어가면 Hydra/Kratos 기동 실패로 이어질 수 있습니다.
|
||||||
```
|
|
||||||
|
잘못된 예:
|
||||||
|
```env
|
||||||
|
USERFRONT_URL=https://sso.example.com # 이렇게 같은 줄 주석 금지
|
||||||
|
```
|
||||||
|
|
||||||
|
올바른 예:
|
||||||
|
```env
|
||||||
|
# UserFront 공개 URL
|
||||||
|
USERFRONT_URL=https://sso.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.env` 핵심 변수 가이드
|
||||||
|
- `IDP_PROVIDER`: 기본 `ory`
|
||||||
|
- `USERFRONT_URL`: 브라우저 기준 공개 도메인 (예: `https://sso.example.com`)
|
||||||
|
- `OATHKEEPER_PUBLIC_URL`: 보통 `${USERFRONT_URL}`
|
||||||
|
- `HYDRA_PUBLIC_URL`: 보통 `${OATHKEEPER_PUBLIC_URL}/oidc`
|
||||||
|
- `KRATOS_BROWSER_URL`: 보통 `${OATHKEEPER_PUBLIC_URL}/auth`
|
||||||
|
- `KRATOS_UI_URL`: UserFront UI URL (로컬 예: `http://localhost:5000`)
|
||||||
|
- `ADMINFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5173/auth/callback`)
|
||||||
|
- `DEVFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5174/callback`)
|
||||||
|
- 주의: callback URL 끝에 `/`가 붙으면 `make validate-auth-config`에서 실패 처리됩니다.
|
||||||
|
- `KRATOS_ALLOWED_RETURN_URLS_EXTRA`: 추가 허용 return URL (선택)
|
||||||
|
- 빈값: `[]`
|
||||||
|
- 다중값: `["https://a.example.com/callback","https://b.example.com/callback"]` 또는 `https://a.example.com/callback,https://b.example.com/callback`
|
||||||
|
|
||||||
|
### `.env` 작성 후 권장 점검
|
||||||
|
```bash
|
||||||
|
make validate-auth-config
|
||||||
|
```
|
||||||
|
위 검증은 callback/allowed_return_urls/게이트웨이 매핑 규칙을 점검하고 `.generated/auth-config.env`를 생성합니다.
|
||||||
|
|
||||||
### 전체 스택 실행 (Running the Stack)
|
### 전체 스택 실행 (Running the Stack)
|
||||||
|
|
||||||
@@ -207,6 +237,15 @@ make verify-auth-config
|
|||||||
- 게이트웨이 경유 환경은 URL 문자열 완전일치 대신 매핑 유효성(`direct_match` / `mapped_match`) 기준으로 검증합니다.
|
- 게이트웨이 경유 환경은 URL 문자열 완전일치 대신 매핑 유효성(`direct_match` / `mapped_match`) 기준으로 검증합니다.
|
||||||
- 관련 정책 문서: `docs/oidc_redirect_mapping_validation_policy.md`
|
- 관련 정책 문서: `docs/oidc_redirect_mapping_validation_policy.md`
|
||||||
|
|
||||||
|
### 권장 실행 순서
|
||||||
|
```bash
|
||||||
|
cp .env.sample .env
|
||||||
|
# .env 편집
|
||||||
|
make validate-auth-config
|
||||||
|
make up-dev
|
||||||
|
make up-app
|
||||||
|
```
|
||||||
|
|
||||||
직접 Compose를 사용하려면 다음처럼 env 파일을 함께 주입하세요.
|
직접 Compose를 사용하려면 다음처럼 env 파일을 함께 주입하세요.
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file .env --env-file .generated/auth-config.env -f compose.infra.yaml -f compose.ory.yaml up -d
|
docker compose --env-file .env --env-file .generated/auth-config.env -f compose.infra.yaml -f compose.ory.yaml up -d
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ type MockTenantRepoForSvc struct {
|
|||||||
func (m *MockTenantRepoForSvc) Create(ctx context.Context, tenant *domain.Tenant) error {
|
func (m *MockTenantRepoForSvc) Create(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
return m.Called(ctx, tenant).Error(0)
|
return m.Called(ctx, tenant).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) Update(ctx context.Context, tenant *domain.Tenant) error {
|
func (m *MockTenantRepoForSvc) Update(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
return m.Called(ctx, tenant).Error(0)
|
return m.Called(ctx, tenant).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
func (m *MockTenantRepoForSvc) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||||
args := m.Called(ctx, id)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -30,7 +28,6 @@ func (m *MockTenantRepoForSvc) FindByID(ctx context.Context, id string) (*domain
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
func (m *MockTenantRepoForSvc) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||||
args := m.Called(ctx, slug)
|
args := m.Called(ctx, slug)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -38,11 +35,9 @@ func (m *MockTenantRepoForSvc) FindBySlug(ctx context.Context, slug string) (*do
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
func (m *MockTenantRepoForSvc) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
func (m *MockTenantRepoForSvc) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||||
args := m.Called(ctx, domainName)
|
args := m.Called(ctx, domainName)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -50,11 +45,9 @@ func (m *MockTenantRepoForSvc) FindByDomain(ctx context.Context, domainName stri
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
func (m *MockTenantRepoForSvc) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantRepoForSvc) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
|
func (m *MockTenantRepoForSvc) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
|
||||||
return m.Called(ctx, tenantID, domainName, verified).Error(0)
|
return m.Called(ctx, tenantID, domainName, verified).Error(0)
|
||||||
}
|
}
|
||||||
@@ -66,21 +59,17 @@ type MockKetoSvcForTenant struct {
|
|||||||
func (m *MockKetoSvcForTenant) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
func (m *MockKetoSvcForTenant) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKetoSvcForTenant) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
func (m *MockKetoSvcForTenant) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKetoSvcForTenant) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]RelationTuple, error) {
|
func (m *MockKetoSvcForTenant) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]RelationTuple, error) {
|
||||||
args := m.Called(ctx, namespace, object, relation, subject)
|
args := m.Called(ctx, namespace, object, relation, subject)
|
||||||
return args.Get(0).([]RelationTuple), args.Error(1)
|
return args.Get(0).([]RelationTuple), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKetoSvcForTenant) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
func (m *MockKetoSvcForTenant) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||||
args := m.Called(ctx, namespace, relation, subject)
|
args := m.Called(ctx, namespace, relation, subject)
|
||||||
return args.Get(0).([]string), args.Error(1)
|
return args.Get(0).([]string), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKetoSvcForTenant) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
|
func (m *MockKetoSvcForTenant) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
|
||||||
args := m.Called(ctx, namespace, object, relation, subject)
|
args := m.Called(ctx, namespace, object, relation, subject)
|
||||||
return args.Bool(0), args.Error(1)
|
return args.Bool(0), args.Error(1)
|
||||||
@@ -99,19 +88,15 @@ func (m *MockUserRepoForTenant) FindByEmail(ctx context.Context, email string) (
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*domain.User), args.Error(1)
|
return args.Get(0).(*domain.User), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserRepoForTenant) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
func (m *MockUserRepoForTenant) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserRepoForTenant) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
func (m *MockUserRepoForTenant) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserRepoForTenant) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
func (m *MockUserRepoForTenant) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) {
|
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,40 +198,40 @@ services:
|
|||||||
|
|
||||||
# 기본 RP (Admin Front 등) 자동 등록 컨테이너
|
# 기본 RP (Admin Front 등) 자동 등록 컨테이너
|
||||||
init-rp:
|
init-rp:
|
||||||
image: oryd/hydra:${HYDRA_VERSION:-v25.4.0}
|
image: oryd/hydra:v25.4.0
|
||||||
environment:
|
entrypoint: ["/bin/sh"]
|
||||||
- HYDRA_ADMIN_URL=http://hydra:4445
|
command:
|
||||||
- OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}
|
- -ec
|
||||||
- OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}
|
- |
|
||||||
- ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback}
|
hydra delete oauth2-client --endpoint http://hydra:4445 adminfront >/dev/null 2>&1 || true
|
||||||
- DEVFRONT_CALLBACK_URLS=${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/callback}
|
hydra delete oauth2-client --endpoint http://hydra:4445 devfront >/dev/null 2>&1 || true
|
||||||
command: |
|
hydra delete oauth2-client --endpoint http://hydra:4445 ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} >/dev/null 2>&1 || true
|
||||||
hydra clients create \
|
|
||||||
--endpoint http://hydra:4445 \
|
|
||||||
--id adminfront \
|
|
||||||
--secret admin-secret \
|
|
||||||
--grant-types authorization_code,refresh_token \
|
|
||||||
--response-types code \
|
|
||||||
--scope openid,offline_access,profile,email \
|
|
||||||
--callbacks "$ADMINFRONT_CALLBACK_URLS";
|
|
||||||
|
|
||||||
hydra clients create \
|
hydra create oauth2-client \
|
||||||
--endpoint http://hydra:4445 \
|
--endpoint http://hydra:4445 \
|
||||||
--id devfront \
|
--id adminfront \
|
||||||
--grant-types authorization_code,refresh_token \
|
--secret admin-secret \
|
||||||
--response-types code \
|
--grant-type authorization_code,refresh_token \
|
||||||
|
--response-type code \
|
||||||
|
--scope openid,offline_access,profile,email \
|
||||||
|
--redirect-uri ${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback}
|
||||||
|
|
||||||
|
hydra create oauth2-client \
|
||||||
|
--endpoint http://hydra:4445 \
|
||||||
|
--id devfront \
|
||||||
|
--grant-type authorization_code,refresh_token \
|
||||||
|
--response-type code \
|
||||||
--scope openid,offline_access,profile,email \
|
--scope openid,offline_access,profile,email \
|
||||||
--token-endpoint-auth-method none \
|
--token-endpoint-auth-method none \
|
||||||
--response-types code \
|
--redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/callback}
|
||||||
--callbacks "$DEVFRONT_CALLBACK_URLS";
|
|
||||||
|
|
||||||
hydra clients create \
|
hydra create oauth2-client \
|
||||||
--endpoint http://hydra:4445 \
|
--endpoint http://hydra:4445 \
|
||||||
--id "$OATHKEEPER_INTROSPECT_CLIENT_ID" \
|
--id ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} \
|
||||||
--secret "$OATHKEEPER_INTROSPECT_CLIENT_SECRET" \
|
--secret ${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} \
|
||||||
--grant-types client_credentials \
|
--grant-type client_credentials \
|
||||||
--response-types token \
|
--response-type token \
|
||||||
--scope openid,offline_access,profile,email;
|
--scope openid,offline_access,profile,email
|
||||||
depends_on:
|
depends_on:
|
||||||
ory_stack_check:
|
ory_stack_check:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|||||||
53
docs/trouble-shooting/dev-branch-conflict-policy.md
Normal file
53
docs/trouble-shooting/dev-branch-conflict-policy.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# dev 브랜치 충돌 대응 정책
|
||||||
|
|
||||||
|
## 현재 상태 점검 기준
|
||||||
|
- `git status -sb` 기준으로 `unmerged paths`가 없으면 파일 단위 충돌은 없는 상태입니다.
|
||||||
|
- `non-fast-forward` push 거절은 로컬/원격 히스토리 분기(diverged) 상태로 간주합니다.
|
||||||
|
- 원격 확인 불가(DNS/네트워크 장애) 시, 로컬 기준 상태를 우선 공유하고 원격 fetch 가능 시점에 재확인합니다.
|
||||||
|
|
||||||
|
## 기본 원칙
|
||||||
|
1. `dev` 반영 전 최신 원격 기준선 확보
|
||||||
|
2. 충돌 해결은 기능 회귀 방지 우선
|
||||||
|
3. 해결 후 CI 강제 검사 통과 확인
|
||||||
|
|
||||||
|
## CI 강제 검사 정책
|
||||||
|
- `.gitea/workflows/code_check.yml`는 아래 이벤트에서 항상 실행됩니다.
|
||||||
|
- `push` to `dev`
|
||||||
|
- `pull_request` targeting `dev`
|
||||||
|
- `workflow_dispatch` (수동 실행)
|
||||||
|
- 수동 실행 입력으로 검사 항목을 끄는 방식은 사용하지 않습니다.
|
||||||
|
- `backend-tests`, `userfront-tests`는 `lint` 결과와 무관하게 실행 시도하여 전체 실패 지점을 한 번에 확인합니다.
|
||||||
|
|
||||||
|
## 표준 절차
|
||||||
|
1. 원격 최신화
|
||||||
|
```bash
|
||||||
|
git fetch origin dev
|
||||||
|
git status -sb
|
||||||
|
git rev-list --left-right --count origin/dev...dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 분기 상태별 처리
|
||||||
|
- 로컬만 앞섬 (`0 N`): `git push origin dev`
|
||||||
|
- 원격만 앞섬 (`N 0`): `git rebase origin/dev` 후 push
|
||||||
|
- 상호 분기 (`N M`): `git rebase origin/dev`로 정렬 후 충돌 해결
|
||||||
|
|
||||||
|
3. 충돌 해결 후 검증
|
||||||
|
```bash
|
||||||
|
make validate-auth-config
|
||||||
|
make verify-auth-config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 우선순위 정책 (이번 범위 #274 / #276)
|
||||||
|
1. OIDC 리다이렉트/쿼리 전달 회귀 방지 로직 유지
|
||||||
|
2. `Makefile` 기반 인증 설정 생성/검증 경로 유지
|
||||||
|
3. `compose.ory.yaml`의 callback/allowed_return_urls env 연동 유지
|
||||||
|
4. `.env` 값 형식 안정성 유지 (same-line 주석 금지)
|
||||||
|
|
||||||
|
## 주의 사항
|
||||||
|
- `dev` 공유 브랜치에서는 `force push`를 사용하지 않습니다.
|
||||||
|
- `.env`에서 `KEY=value #comment` 형태는 금지합니다. (URL 끝 공백으로 Hydra/Kratos 기동 실패 가능)
|
||||||
|
- callback URL 끝 `/`는 `make validate-auth-config`에서 실패 처리됩니다.
|
||||||
|
|
||||||
|
## 관련 문서
|
||||||
|
- `docs/oidc_redirect_mapping_validation_policy.md`
|
||||||
|
- `README.md`
|
||||||
@@ -32,6 +32,24 @@ warn() {
|
|||||||
WARNINGS+=("$1")
|
WARNINGS+=("$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_dotenv_line_safety() {
|
||||||
|
local key="$1"
|
||||||
|
local env_file="$ROOT_DIR/.env"
|
||||||
|
[[ -f "$env_file" ]] || return 0
|
||||||
|
|
||||||
|
local raw_line
|
||||||
|
raw_line="$(grep -E "^${key}=" "$env_file" | tail -n 1 || true)"
|
||||||
|
[[ -n "$raw_line" ]] || return 0
|
||||||
|
|
||||||
|
if [[ "$raw_line" == *" #"* ]]; then
|
||||||
|
fail ".env line for $key contains inline comment. Use comment-only line above the key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$raw_line" =~ [[:space:]]+$ ]]; then
|
||||||
|
fail ".env line for $key has trailing whitespace."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
trim() {
|
trim() {
|
||||||
local value="$1"
|
local value="$1"
|
||||||
value="${value#"${value%%[![:space:]]*}"}"
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
@@ -50,6 +68,38 @@ csv_to_lines() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list_to_lines() {
|
||||||
|
local raw="$1"
|
||||||
|
raw="$(trim "$raw")"
|
||||||
|
if [[ -z "$raw" || "$raw" == "[]" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$raw" =~ ^\[(.*)\]$ ]]; then
|
||||||
|
local inner="${BASH_REMATCH[1]}"
|
||||||
|
inner="$(trim "$inner")"
|
||||||
|
if [[ -z "$inner" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$inner" | tr ',' '\n' | while IFS= read -r token; do
|
||||||
|
local item
|
||||||
|
item="$(trim "$token")"
|
||||||
|
item="${item#\"}"
|
||||||
|
item="${item%\"}"
|
||||||
|
item="${item#\'}"
|
||||||
|
item="${item%\'}"
|
||||||
|
item="$(trim "$item")"
|
||||||
|
if [[ -n "$item" ]]; then
|
||||||
|
printf '%s\n' "$item"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
csv_to_lines "$raw"
|
||||||
|
}
|
||||||
|
|
||||||
is_http_url() {
|
is_http_url() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
[[ "$url" =~ ^https?://[^[:space:]]+$ ]]
|
[[ "$url" =~ ^https?://[^[:space:]]+$ ]]
|
||||||
@@ -144,7 +194,7 @@ collect_values() {
|
|||||||
|
|
||||||
while IFS= read -r item; do
|
while IFS= read -r item; do
|
||||||
EXTRA_ALLOWED_RETURNS+=("$item")
|
EXTRA_ALLOWED_RETURNS+=("$item")
|
||||||
done < <(csv_to_lines "$KRATOS_ALLOWED_RETURN_URLS_EXTRA")
|
done < <(list_to_lines "$KRATOS_ALLOWED_RETURN_URLS_EXTRA")
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_urls() {
|
validate_urls() {
|
||||||
@@ -169,8 +219,13 @@ validate_callback_group() {
|
|||||||
local has_path=0
|
local has_path=0
|
||||||
for url in "${urls[@]}"; do
|
for url in "${urls[@]}"; do
|
||||||
validate_urls "$group_name entry" "$url"
|
validate_urls "$group_name entry" "$url"
|
||||||
|
local canonical
|
||||||
|
canonical="$(canonicalize_url "$url")"
|
||||||
|
if [[ "$url" != "$canonical" ]]; then
|
||||||
|
fail "$group_name entry must not end with trailing slash: $url"
|
||||||
|
fi
|
||||||
local path
|
local path
|
||||||
path="$(url_path "$url")"
|
path="$(url_path "$canonical")"
|
||||||
if [[ -n "$path" && "$path" != "/" ]]; then
|
if [[ -n "$path" && "$path" != "/" ]]; then
|
||||||
has_path=1
|
has_path=1
|
||||||
fi
|
fi
|
||||||
@@ -283,9 +338,9 @@ EOF
|
|||||||
validate_compose_wiring() {
|
validate_compose_wiring() {
|
||||||
grep -Eq 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=\$\{KRATOS_ALLOWED_RETURN_URLS_JSON' "$ROOT_DIR/compose.ory.yaml" \
|
grep -Eq 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=\$\{KRATOS_ALLOWED_RETURN_URLS_JSON' "$ROOT_DIR/compose.ory.yaml" \
|
||||||
|| fail "compose.ory.yaml is not wired to KRATOS_ALLOWED_RETURN_URLS_JSON"
|
|| fail "compose.ory.yaml is not wired to KRATOS_ALLOWED_RETURN_URLS_JSON"
|
||||||
grep -Eq 'ADMINFRONT_CALLBACK_URLS=\$\{ADMINFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|
grep -Eq 'ADMINFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|
||||||
|| fail "compose.ory.yaml is not wired to ADMINFRONT_CALLBACK_URLS"
|
|| fail "compose.ory.yaml is not wired to ADMINFRONT_CALLBACK_URLS"
|
||||||
grep -Eq 'DEVFRONT_CALLBACK_URLS=\$\{DEVFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|
grep -Eq 'DEVFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|
||||||
|| fail "compose.ory.yaml is not wired to DEVFRONT_CALLBACK_URLS"
|
|| fail "compose.ory.yaml is not wired to DEVFRONT_CALLBACK_URLS"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,10 +356,10 @@ verify_runtime_hydra_clients() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local admin_info dev_info
|
local admin_info dev_info
|
||||||
if ! admin_info="$(docker exec ory_hydra hydra clients get --endpoint http://hydra:4445 adminfront 2>/dev/null)"; then
|
if ! admin_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 adminfront 2>/dev/null)"; then
|
||||||
fail "failed to read hydra client 'adminfront' from running container"
|
fail "failed to read hydra client 'adminfront' from running container"
|
||||||
fi
|
fi
|
||||||
if ! dev_info="$(docker exec ory_hydra hydra clients get --endpoint http://hydra:4445 devfront 2>/dev/null)"; then
|
if ! dev_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 devfront 2>/dev/null)"; then
|
||||||
fail "failed to read hydra client 'devfront' from running container"
|
fail "failed to read hydra client 'devfront' from running container"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -321,6 +376,15 @@ verify_runtime_hydra_clients() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_validation() {
|
run_validation() {
|
||||||
|
validate_dotenv_line_safety "USERFRONT_URL"
|
||||||
|
validate_dotenv_line_safety "BACKEND_URL"
|
||||||
|
validate_dotenv_line_safety "OATHKEEPER_PUBLIC_URL"
|
||||||
|
validate_dotenv_line_safety "HYDRA_PUBLIC_URL"
|
||||||
|
validate_dotenv_line_safety "KRATOS_BROWSER_URL"
|
||||||
|
validate_dotenv_line_safety "KRATOS_UI_URL"
|
||||||
|
validate_dotenv_line_safety "ADMINFRONT_CALLBACK_URLS"
|
||||||
|
validate_dotenv_line_safety "DEVFRONT_CALLBACK_URLS"
|
||||||
|
|
||||||
collect_values
|
collect_values
|
||||||
validate_callback_group "ADMINFRONT_CALLBACK_URLS" "/auth/callback" "${ADMIN_CALLBACKS[@]}"
|
validate_callback_group "ADMINFRONT_CALLBACK_URLS" "/auth/callback" "${ADMIN_CALLBACKS[@]}"
|
||||||
validate_callback_group "DEVFRONT_CALLBACK_URLS" "/callback" "${DEV_CALLBACKS[@]}"
|
validate_callback_group "DEVFRONT_CALLBACK_URLS" "/callback" "${DEV_CALLBACKS[@]}"
|
||||||
|
|||||||
Reference in New Issue
Block a user