mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-15 08:47:46 +00:00
Merge branch 'main' into next-rc
# Conflicts: # internal/api/http/middleware/instance_interceptor.go # internal/api/http/middleware/instance_interceptor_test.go
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,6 +25,7 @@ sandbox.go
|
||||
.idea
|
||||
.vscode
|
||||
.DS_STORE
|
||||
.run
|
||||
|
||||
# credential
|
||||
google-credentials
|
||||
|
2
Makefile
2
Makefile
@@ -103,7 +103,7 @@ core_unit_test:
|
||||
core_integration_setup:
|
||||
go build -o zitadel main.go
|
||||
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/zitadel.yaml --steps internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||
$(RM) zitadel
|
||||
|
||||
.PHONY: core_integration_test
|
||||
|
@@ -838,6 +838,11 @@ DefaultInstance:
|
||||
# DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org
|
||||
# If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests.
|
||||
DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION
|
||||
# AllowedLanguages restricts the languages that can be used.
|
||||
# If the list is empty, all supported languages are allowed.
|
||||
AllowedLanguages: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_ALLOWEDLANGUAGES
|
||||
# - en
|
||||
# - de
|
||||
Quotas:
|
||||
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
||||
# The following unit types are supported
|
||||
|
@@ -1,2 +1,2 @@
|
||||
-- replace %[1]s with the name of the user
|
||||
CREATE USER IF NOT EXISTS %[1]s
|
||||
CREATE USER IF NOT EXISTS "%[1]s"
|
@@ -1,2 +1,2 @@
|
||||
-- replace %[1]s with the name of the database
|
||||
CREATE DATABASE IF NOT EXISTS %[1]s
|
||||
CREATE DATABASE IF NOT EXISTS "%[1]s"
|
@@ -1,4 +1,4 @@
|
||||
-- replace the first %[1]s with the database
|
||||
-- replace the second \%[2]s with the user
|
||||
GRANT ALL ON DATABASE %[1]s TO %[2]s;
|
||||
GRANT SYSTEM VIEWACTIVITY TO %[2]s;
|
||||
GRANT ALL ON DATABASE "%[1]s" TO "%[2]s";
|
||||
GRANT SYSTEM VIEWACTIVITY TO "%[2]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS eventstore;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS projections;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS system;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s";
|
@@ -1 +1 @@
|
||||
CREATE USER %[1]s
|
||||
CREATE USER "%[1]s"
|
@@ -1 +1 @@
|
||||
CREATE DATABASE %[1]s
|
||||
CREATE DATABASE "%[1]s"
|
@@ -1,3 +1,3 @@
|
||||
-- replace the first %[1]s with the database
|
||||
-- replace the second \%[2]s with the user
|
||||
GRANT ALL ON DATABASE %[1]s TO %[2]s;
|
||||
GRANT ALL ON DATABASE "%[1]s" TO "%[2]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS eventstore;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS projections;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s";
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS system;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s";
|
@@ -26,7 +26,7 @@ func Test_verifyDB(t *testing.T) {
|
||||
name: "doesn't exists, create fails",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", sql.ErrTxDone),
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone),
|
||||
),
|
||||
database: "zitadel",
|
||||
},
|
||||
@@ -36,7 +36,7 @@ func Test_verifyDB(t *testing.T) {
|
||||
name: "doesn't exists, create successful",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil),
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil),
|
||||
),
|
||||
database: "zitadel",
|
||||
},
|
||||
@@ -46,7 +46,7 @@ func Test_verifyDB(t *testing.T) {
|
||||
name: "already exists",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil),
|
||||
expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil),
|
||||
),
|
||||
database: "zitadel",
|
||||
},
|
||||
|
@@ -21,7 +21,7 @@ func Test_verifyGrant(t *testing.T) {
|
||||
name: "doesn't exists, create fails",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", sql.ErrTxDone),
|
||||
expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", sql.ErrTxDone),
|
||||
),
|
||||
database: "zitadel",
|
||||
username: "zitadel-user",
|
||||
@@ -32,7 +32,7 @@ func Test_verifyGrant(t *testing.T) {
|
||||
name: "correct",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil),
|
||||
expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil),
|
||||
),
|
||||
database: "zitadel",
|
||||
username: "zitadel-user",
|
||||
@@ -43,7 +43,7 @@ func Test_verifyGrant(t *testing.T) {
|
||||
name: "already exists",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil),
|
||||
expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil),
|
||||
),
|
||||
database: "zitadel",
|
||||
username: "zitadel-user",
|
||||
|
@@ -27,7 +27,7 @@ func Test_verifyUser(t *testing.T) {
|
||||
name: "doesn't exists, create fails",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", sql.ErrTxDone),
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone),
|
||||
),
|
||||
username: "zitadel-user",
|
||||
password: "",
|
||||
@@ -38,7 +38,7 @@ func Test_verifyUser(t *testing.T) {
|
||||
name: "correct without password",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", nil),
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil),
|
||||
),
|
||||
username: "zitadel-user",
|
||||
password: "",
|
||||
@@ -49,7 +49,7 @@ func Test_verifyUser(t *testing.T) {
|
||||
name: "correct with password",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil),
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil),
|
||||
),
|
||||
username: "zitadel-user",
|
||||
password: "password",
|
||||
@@ -60,7 +60,7 @@ func Test_verifyUser(t *testing.T) {
|
||||
name: "already exists",
|
||||
args: args{
|
||||
db: prepareDB(t,
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil),
|
||||
expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil),
|
||||
),
|
||||
username: "zitadel-user",
|
||||
password: "",
|
||||
|
@@ -1,3 +1,3 @@
|
||||
CREATE SCHEMA IF NOT EXISTS logstore;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA logstore TO %[1]s;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA logstore TO "%[1]s";
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/migration"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
)
|
||||
@@ -64,6 +65,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
||||
ctx := context.Background()
|
||||
logging.Info("setup started")
|
||||
|
||||
i18n.MustLoadSupportedLanguagesFromDir()
|
||||
|
||||
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
||||
logging.OnError(err).Fatal("unable to connect to database")
|
||||
esPusherDBClient, err := database.Connect(config.Database, false, true)
|
||||
|
@@ -62,6 +62,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/emitters/access"
|
||||
@@ -93,7 +94,6 @@ Requirements:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return startZitadel(config, masterKey, server)
|
||||
},
|
||||
}
|
||||
@@ -123,6 +123,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
i18n.MustLoadSupportedLanguagesFromDir()
|
||||
|
||||
zitadelDBClient, err := database.Connect(config.Database, false, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start client for projection: %w", err)
|
||||
@@ -215,6 +217,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
}
|
||||
defer commands.Close(ctx) // wait for background jobs
|
||||
|
||||
clock := clockpkg.New()
|
||||
actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]())
|
||||
|
@@ -83,8 +83,8 @@ export class ActionTableComponent implements OnInit {
|
||||
this.mgmtService
|
||||
.deleteAction(action.id)
|
||||
.then(() => {
|
||||
this.selection.clear();
|
||||
this.toast.showInfo('FLOWS.DIALOG.DELETEACTION.DELETE_SUCCESS', true);
|
||||
|
||||
this.refreshPage();
|
||||
})
|
||||
.catch((error: any) => {
|
||||
|
@@ -309,6 +309,7 @@ export class UserTableComponent implements OnInit {
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
}, 1000);
|
||||
this.selection.clear();
|
||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@@ -8,9 +8,26 @@ OAuth 2 Token Introspection.
|
||||
|
||||
At the end of the guide you should have an API with a protected endpoint.
|
||||
|
||||
> This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/tree/authorization/example/api).
|
||||
|
||||
## Set up application and obtain keys
|
||||
|
||||
Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console.
|
||||
You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page.
|
||||
Select the **API** application type and continue.
|
||||
|
||||

|
||||
|
||||
We recommend that you use JWT Profile for authenticating at the Introspection Endpoint.
|
||||
|
||||

|
||||
|
||||
Then create a new key with your desired expiration date. Be sure to download it, as you won't be able to retrieve it again.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
The client [SDK](https://github.com/zitadel/zitadel-go) will provides an interceptor for both GRPC and HTTP.
|
||||
This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc).
|
||||
All that is required, is to create your API and download the private key file later called `Key JSON` for the service user.
|
||||
|
||||
@@ -18,134 +35,170 @@ All that is required, is to create your API and download the private key file la
|
||||
|
||||
### Add Go SDK to your project
|
||||
|
||||
You need to add the SDK into Go Modules by:
|
||||
You need to add the [SDK](https://github.com/zitadel/zitadel-go) into Go Modules by:
|
||||
|
||||
```bash
|
||||
go get github.com/zitadel/zitadel-go/v2
|
||||
go get -u github.com/zitadel/zitadel-go/v3
|
||||
```
|
||||
|
||||
### Create example API
|
||||
|
||||
Create a new go file with the content below. This will create an API with two endpoints. On path `/public` it will always write
|
||||
back `ok` and the current timestamp. On `/protected` it will respond the same but only if a valid access_token is sent. The token
|
||||
must not be expired and the API has to be part of the audience (either client_id or project_id).
|
||||
Create a new go file with the content below. This will create an API with three endpoints:
|
||||
- `/api/healthz`: can be called by anyone and always returns `OK`
|
||||
- `/api/tasks`: requires authorization and returns the available tasks
|
||||
- `/api/add-task`: requires authorization with granted `admin` role and adds the task to the list
|
||||
|
||||
Make sure to fill the var `issuer` with your own domain. This is the domain of your instance you can find it on the instance detail in the ZITADEL Cloud Customer Portal or in the ZITADEL Console.
|
||||
```go
|
||||
package main
|
||||
If authorization is required, the token must not be expired and the API has to be part of the audience (either client_id or project_id).
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel-go/v2/pkg/api/middleware/http"
|
||||
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
|
||||
)
|
||||
|
||||
var (
|
||||
issuer = flag.String("issuer", "", "issuer of your ZITADEL instance (in the form: https://<instance>.zitadel.cloud or https://<yourdomain>)")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
introspection, err := http_mw.NewIntrospectionInterceptor(*issuer, middleware.OSKeyPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/public", writeOK)
|
||||
router.HandleFunc("/protected", introspection.HandlerFunc(writeOK))
|
||||
|
||||
lis := "127.0.0.1:5001"
|
||||
log.Fatal(http.ListenAndServe(lis, router))
|
||||
}
|
||||
|
||||
func writeOK(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK " + time.Now().String()))
|
||||
}
|
||||
For tests we will use a Personal Access Token.
|
||||
|
||||
```go reference
|
||||
https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go
|
||||
```
|
||||
|
||||
#### Key JSON
|
||||
You will need to provide some values for the program to run:
|
||||
- `domain`: Your ZITADEL instance domain, e.g. https://my-domain.zitadel.cloud
|
||||
- `key`: The path to the downloaded key.json
|
||||
- `port`: The port on which the API will be accessible, default it 8089
|
||||
|
||||
To provide the key JSON to the SDK, simply set an environment variable `ZITADEL_KEY_PATH` with the path to the JSON as value.
|
||||
|
||||
```bash
|
||||
export ZITADEL_KEY_PATH=/Users/test/apikey.json
|
||||
```
|
||||
|
||||
For development purposes you should be able to set this in your IDE.
|
||||
|
||||
If you're not able to set it via environment variable, you can also exchange the `middleware.OSKeyPath()` and pass it directly:
|
||||
|
||||
```go
|
||||
introspection, err := http_mw.NewIntrospectionInterceptor(
|
||||
client.Issuer,
|
||||
"/Users/test/apikey.json",
|
||||
)
|
||||
```
|
||||
|
||||
### Test API
|
||||
## Test API
|
||||
|
||||
After you have configured everything correctly, you can simply start the example by:
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
go run main.go --domain <your domain> --key <path>
|
||||
```
|
||||
|
||||
You can now call the API by browser or curl. Try the public endpoint first:
|
||||
This could look like:
|
||||
|
||||
```bash
|
||||
curl -i localhost:5001/public
|
||||
go run main.go --domain https://my-domain.zitadel.cloud --key ./api.json
|
||||
```
|
||||
|
||||
After you get a successful log:
|
||||
```
|
||||
2023/12/04 10:27:42 INFO server listening, press ctrl+c to stop addr=http://localhost:8089
|
||||
```
|
||||
|
||||
### Public endpoint
|
||||
|
||||
Now you can call the API by browser or curl. Try the healthz endpoint first:
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:8089/api/healthz
|
||||
```
|
||||
|
||||
it should return something like:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Date: Tue, 24 Aug 2021 11:11:17 GMT
|
||||
Content-Length: 59
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 09:29:38 GMT
|
||||
Content-Length: 4
|
||||
|
||||
OK 2021-08-24 13:11:17.135719 +0200 CEST m=+30704.913892168
|
||||
"OK"
|
||||
```
|
||||
|
||||
and the protected:
|
||||
### Task list
|
||||
|
||||
and the task list endpoint:
|
||||
|
||||
```bash
|
||||
curl -i localhost:5001/protected
|
||||
curl -i http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
it will return:
|
||||
|
||||
```
|
||||
HTTP/1.1 401 Unauthorized
|
||||
Content-Type: application/json
|
||||
Date: Tue, 24 Aug 2021 11:13:10 GMT
|
||||
Content-Length: 21
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Mon, 04 Dec 2023 09:41:54 GMT
|
||||
Content-Length: 44
|
||||
|
||||
"auth header missing"
|
||||
unauthorized: authorization header is empty
|
||||
```
|
||||
|
||||
Get a valid access_token for the API. You can achieve this by login into an application of the same project or
|
||||
by explicitly requesting the project_id for the audience by scope `urn:zitadel:iam:org:project:id:{projectid}:aud`.
|
||||
Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience
|
||||
or use a PAT of a service account.
|
||||
|
||||
If you provide a valid Bearer Token:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" localhost:5001/protected
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
it will return an OK response as well:
|
||||
it will return an empty list:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Date: Tue, 24 Aug 2021 11:13:33 GMT
|
||||
Content-Length: 59
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 09:49:06 GMT
|
||||
Content-Length: 2
|
||||
|
||||
OK 2021-08-24 13:13:33.131943 +0200 CEST m=+30840.911149251
|
||||
{}
|
||||
```
|
||||
|
||||
### Try to add a new task
|
||||
|
||||
Let's see what happens if you call the AddTask endpoint:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task
|
||||
```
|
||||
|
||||
it will complain about the missing `admin` role:
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Mon, 04 Dec 2023 09:52:00 GMT
|
||||
Content-Length: 50
|
||||
|
||||
permission denied: missing required role: `admin`
|
||||
```
|
||||
|
||||
### Add admin role
|
||||
|
||||
So let's create the role and grant it to the user. To do so, go to your project in ZITADEL Console
|
||||
and create the role by selecting `Roles` in the navigation and then clicking on the `New Role` button.
|
||||
Finally, create the role as shown below:
|
||||
|
||||

|
||||
|
||||
After you have created the role, let's grant it the user, who requested the tasks.
|
||||
Click on `Authorization` in the navigation and create a new one by selecting the user and the `admin` role.
|
||||
After successful creation, it should look like:
|
||||
|
||||

|
||||
|
||||
So you should now be able to add a new task:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task --data "task=My new task"
|
||||
```
|
||||
|
||||
which will report back the successful addition:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 10:06:29 GMT
|
||||
Content-Length: 26
|
||||
|
||||
"task `My new task` added"
|
||||
```
|
||||
|
||||
Let's now retrieve the task list again:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
As you can see your new task ist listed. And since you're an `admin` now, you will always get an additional `create a new task on /api/add-task`:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 10:08:38 GMT
|
||||
Content-Length: 62
|
||||
|
||||
{"tasks":["My new task","create a new task on /api/add-task"]}
|
||||
```
|
||||
|
@@ -8,7 +8,11 @@ Users with the role IAM_OWNER can change the restrictions of their instance usin
|
||||
Currently, the following restrictions are available:
|
||||
|
||||
- *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests.
|
||||
- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages*
|
||||
- *AllowedLanguages* - The following rules apply if languages are restricted:
|
||||
- Only allowed languages are listed in the OIDC discovery endpoint */.well-kown/openid-configuration*.
|
||||
- Login UI texts are only rendered in allowed languages.
|
||||
- Notification message texts are only rendered in allowed languages.
|
||||
- Custom Texts can be created for disallowed languages as long as ZITADEL supports that language. Therefore, all texts can be customized before allowing a language.
|
||||
|
||||
Feature restrictions for an instance are intended to be configured by a user that is managed within that instance.
|
||||
However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users.
|
||||
|
@@ -12,6 +12,8 @@ Date: Calendar week 41/42 2023
|
||||
|
||||
Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command.
|
||||
|
||||
Cockroach versions 22.2 < 22.2.11 and 23.1 < 23.1.4 will fail the migration. Please make sure to upgrade to more recent versions first. ZITADEL recommends to use the latest stable version of Cockroachdb.
|
||||
|
||||
## Statement
|
||||
|
||||
To query correct order of events the cockroach database user of ZITADEL needs additional privileges to query the `crdb_internal.cluster_transactions`-table
|
||||
@@ -20,6 +22,8 @@ To query correct order of events the cockroach database user of ZITADEL needs ad
|
||||
|
||||
Before migrating to versions >= 2.39.0 make sure the cockroach database user has sufficient grants.
|
||||
|
||||
Cockroachdb version is up to date.
|
||||
|
||||
## Impact
|
||||
|
||||
If the user doesn't have sufficient grants, events won't be updated.
|
||||
|
BIN
docs/static/img/go/api-create-auth.png
vendored
Normal file
BIN
docs/static/img/go/api-create-auth.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
BIN
docs/static/img/go/api-create-key.png
vendored
Normal file
BIN
docs/static/img/go/api-create-key.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
docs/static/img/go/api-create.png
vendored
Normal file
BIN
docs/static/img/go/api-create.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
BIN
docs/static/img/go/api-project-auth.png
vendored
Normal file
BIN
docs/static/img/go/api-project-auth.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
docs/static/img/go/api-project-role.png
vendored
Normal file
BIN
docs/static/img/go/api-project-role.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
13
go.mod
13
go.mod
@@ -13,6 +13,7 @@ require (
|
||||
github.com/allegro/bigcache v1.2.1
|
||||
github.com/benbjohnson/clock v1.3.5
|
||||
github.com/boombuler/barcode v1.0.1
|
||||
github.com/brianvoe/gofakeit/v6 v6.25.0
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.5
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
|
||||
github.com/crewjam/saml v0.4.14
|
||||
@@ -49,7 +50,6 @@ require (
|
||||
github.com/muhlemmer/gu v0.3.1
|
||||
github.com/muhlemmer/httpforwarded v0.1.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/rs/cors v1.10.1
|
||||
@@ -60,7 +60,7 @@ require (
|
||||
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
|
||||
github.com/ttacon/libphonenumber v1.2.1
|
||||
github.com/zitadel/logging v0.5.0
|
||||
github.com/zitadel/oidc/v3 v3.4.0
|
||||
github.com/zitadel/oidc/v3 v3.5.0
|
||||
github.com/zitadel/passwap v0.4.0
|
||||
github.com/zitadel/saml v0.1.2
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0
|
||||
@@ -74,10 +74,10 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.20.0
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
go.uber.org/mock v0.3.0
|
||||
golang.org/x/crypto v0.15.0
|
||||
golang.org/x/crypto v0.16.0
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678
|
||||
golang.org/x/net v0.18.0
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
golang.org/x/net v0.19.0
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
golang.org/x/sync v0.5.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/api v0.150.0
|
||||
@@ -107,6 +107,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -201,7 +202,7 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
golang.org/x/sys v0.14.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
21
go.sum
21
go.sum
@@ -122,6 +122,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk=
|
||||
github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
@@ -865,8 +867,8 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
|
||||
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA=
|
||||
github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE=
|
||||
github.com/zitadel/oidc/v3 v3.4.0 h1:JkbNnrk/7IG+NOBoZp/P0kx6tPcBvnCekSqDTPCOok4=
|
||||
github.com/zitadel/oidc/v3 v3.4.0/go.mod h1:jUnLnx5ihKlo88cSEduZkKlzeMrjzcWVZ8fTzKBxZKY=
|
||||
github.com/zitadel/oidc/v3 v3.5.0 h1:z51AN6FPo5UuwYJ1r9nLvHlxpTGYd8QXg5MrtYm/dgM=
|
||||
github.com/zitadel/oidc/v3 v3.5.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ=
|
||||
github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0=
|
||||
github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g=
|
||||
github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo=
|
||||
@@ -951,8 +953,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1040,8 +1042,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1051,8 +1053,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
|
||||
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1133,8 +1135,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,29 +3,23 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/text"
|
||||
caos_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
|
||||
langs, err := s.query.Languages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
|
||||
return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) {
|
||||
lang, err := language.Parse(req.Language)
|
||||
lang, err := domain.ParseLanguage(req.Language)
|
||||
if err != nil {
|
||||
return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse")
|
||||
return nil, err
|
||||
}
|
||||
details, err := s.command.SetDefaultLanguage(ctx, lang)
|
||||
details, err := s.command.SetDefaultLanguage(ctx, lang[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
19
internal/api/grpc/admin/language_converter.go
Normal file
19
internal/api/grpc/admin/language_converter.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) {
|
||||
allowedLanguages := languages.GetList()
|
||||
if allowedLanguages == nil && languages != nil {
|
||||
allowedLanguages = make([]string, 0)
|
||||
}
|
||||
if allowedLanguages == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return domain.ParseLanguage(allowedLanguages...)
|
||||
}
|
@@ -75,7 +75,6 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine
|
||||
|
||||
createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{
|
||||
Name: req.Org.Name,
|
||||
CustomDomain: req.Org.Domain,
|
||||
|
@@ -5,11 +5,19 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) {
|
||||
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration})
|
||||
lang, err := selectLanguagesToCommand(req.GetAllowedLanguages())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{
|
||||
DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration,
|
||||
AllowedLanguages: lang,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe
|
||||
return &admin.GetRestrictionsResponse{
|
||||
Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner),
|
||||
DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration,
|
||||
AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages),
|
||||
}, nil
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
browserSession := &http.Client{Jar: jar}
|
||||
// Default should be allowed
|
||||
csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
|
||||
require.NoError(t, err)
|
||||
awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
|
||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
|
||||
require.NoError(t, err)
|
||||
awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
||||
var csrfToken string
|
||||
t.Run("public org registration is allowed by default", func(*testing.T) {
|
||||
csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
||||
})
|
||||
t.Run("disallowing public org registration disables the endpoints", func(*testing.T) {
|
||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
|
||||
require.NoError(t, err)
|
||||
awaitPubOrgRegDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken)
|
||||
})
|
||||
t.Run("allowing public org registration again re-enables the endpoints", func(*testing.T) {
|
||||
_, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
|
||||
require.NoError(t, err)
|
||||
awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl)
|
||||
})
|
||||
}
|
||||
|
||||
// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
|
||||
func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
|
||||
csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK)
|
||||
// awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
|
||||
func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string {
|
||||
csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK)
|
||||
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
|
||||
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
||||
require.NoError(t, err)
|
||||
@@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed
|
||||
return csrfToken
|
||||
}
|
||||
|
||||
// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
|
||||
func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
|
||||
awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
|
||||
// awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
|
||||
func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
|
||||
awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
|
||||
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
|
||||
restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, restrictions.DisallowPublicOrgRegistration)
|
||||
}
|
||||
|
||||
// awaitGetResponse cuts the CSRF token from the response body if it exists
|
||||
func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
|
||||
// awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists
|
||||
func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
|
||||
var csrfToken []byte
|
||||
await(t, ctx, func() bool {
|
||||
resp, err := client.Get(parsedURL.String())
|
||||
@@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa
|
||||
if hasCsrfToken {
|
||||
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
|
||||
}
|
||||
return resp.StatusCode == expectCode
|
||||
return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
|
||||
})
|
||||
return string(csrfToken)
|
||||
}
|
||||
@@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien
|
||||
"gorilla.csrf.Token": {csrfToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return resp.StatusCode == expectCode
|
||||
|
||||
return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode)
|
||||
})
|
||||
}
|
||||
|
||||
func await(t *testing.T, ctx context.Context, cb func() bool) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok, "context must have deadline")
|
||||
require.Eventuallyf(
|
||||
t,
|
||||
func() bool {
|
||||
defer func() {
|
||||
require.Nil(t, recover(), "panic in await callback")
|
||||
}()
|
||||
return cb()
|
||||
},
|
||||
time.Until(deadline),
|
||||
100*time.Millisecond,
|
||||
"awaiting successful callback failed",
|
||||
)
|
||||
}
|
@@ -0,0 +1,258 @@
|
||||
//go:build integration
|
||||
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/text"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
|
||||
var (
|
||||
defaultAndAllowedLanguage = language.German
|
||||
supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()}
|
||||
disallowedLanguage = language.Spanish
|
||||
unsupportedLanguage1 = language.Afrikaans
|
||||
unsupportedLanguage2 = language.Albanian
|
||||
)
|
||||
|
||||
domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX)
|
||||
t.Run("assumed defaults are correct", func(tt *testing.T) {
|
||||
tt.Run("languages are not restricted by default", func(ttt *testing.T) {
|
||||
restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})
|
||||
require.NoError(ttt, err)
|
||||
require.Len(ttt, restrictions.AllowedLanguages, 0)
|
||||
})
|
||||
tt.Run("default language is English by default", func(ttt *testing.T) {
|
||||
defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{})
|
||||
require.NoError(ttt, err)
|
||||
require.Equal(ttt, language.Make(defaultLang.Language), language.English)
|
||||
})
|
||||
tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) {
|
||||
checkDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil)
|
||||
})
|
||||
})
|
||||
t.Run("restricting the default language fails", func(tt *testing.T) {
|
||||
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}})
|
||||
expectStatus, ok := status.FromError(err)
|
||||
require.True(tt, ok)
|
||||
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
|
||||
})
|
||||
t.Run("not defining any restrictions throws an error", func(tt *testing.T) {
|
||||
_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{})
|
||||
expectStatus, ok := status.FromError(err)
|
||||
require.True(tt, ok)
|
||||
require.Equal(tt, codes.InvalidArgument, expectStatus.Code())
|
||||
})
|
||||
t.Run("setting the default language works", func(tt *testing.T) {
|
||||
setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage)
|
||||
})
|
||||
t.Run("restricting allowed languages works", func(tt *testing.T) {
|
||||
setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()})
|
||||
})
|
||||
t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) {
|
||||
_, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()})
|
||||
expectStatus, ok := status.FromError(err)
|
||||
require.True(tt, ok)
|
||||
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
|
||||
})
|
||||
t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) {
|
||||
supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{})
|
||||
require.NoError(tt, err)
|
||||
require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr))
|
||||
})
|
||||
t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) {
|
||||
checkDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
|
||||
})
|
||||
t.Run("the login ui is rendered in the default language", func(tt *testing.T) {
|
||||
checkLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz")
|
||||
})
|
||||
t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) {
|
||||
var importedUser *management.ImportHumanUserResponse
|
||||
tt.Run("import user", func(ttt *testing.T) {
|
||||
var err error
|
||||
importedUser, err = importUser(iamOwnerCtx, unsupportedLanguage1)
|
||||
require.NoError(ttt, err)
|
||||
})
|
||||
tt.Run("change user profile", func(ttt *testing.T) {
|
||||
_, err := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{
|
||||
UserId: importedUser.GetUserId(),
|
||||
FirstName: "hodor",
|
||||
LastName: "hodor",
|
||||
NickName: integration.RandString(5),
|
||||
DisplayName: "hodor",
|
||||
PreferredLanguage: unsupportedLanguage2.String(),
|
||||
Gender: user.Gender_GENDER_MALE,
|
||||
})
|
||||
require.NoError(ttt, err)
|
||||
})
|
||||
})
|
||||
t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) {
|
||||
_, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{
|
||||
Language: disallowedLanguage.String(),
|
||||
EmailVerificationText: &text.EmailVerificationScreenText{
|
||||
Description: "hodor",
|
||||
},
|
||||
})
|
||||
assert.NoError(tt, err)
|
||||
_, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{
|
||||
Language: disallowedLanguage.String(),
|
||||
EmailVerificationText: &text.EmailVerificationScreenText{
|
||||
Description: "hodor",
|
||||
},
|
||||
})
|
||||
assert.NoError(tt, err)
|
||||
_, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{
|
||||
Language: disallowedLanguage.String(),
|
||||
Text: "hodor",
|
||||
})
|
||||
assert.NoError(tt, err)
|
||||
_, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{
|
||||
Language: disallowedLanguage.String(),
|
||||
Text: "hodor",
|
||||
})
|
||||
assert.NoError(tt, err)
|
||||
})
|
||||
t.Run("allowing all languages works", func(tt *testing.T) {
|
||||
tt.Run("restricting allowed languages works", func(ttt *testing.T) {
|
||||
setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("allowing the language makes it usable again", func(tt *testing.T) {
|
||||
tt.Run("the disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) {
|
||||
checkDiscoveryEndpoint(ttt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
|
||||
})
|
||||
tt.Run("the login ui is rendered in the allowed language", func(ttt *testing.T) {
|
||||
checkLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) {
|
||||
_, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}})
|
||||
require.NoError(t, err)
|
||||
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer awaitCancel()
|
||||
await(t, awaitCtx, func() bool {
|
||||
restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{})
|
||||
expectLanguages := selectLanguages
|
||||
if len(selectLanguages) == 0 {
|
||||
expectLanguages = nil
|
||||
}
|
||||
return assert.NoError(NoopAssertionT, getErr) &&
|
||||
assert.Equal(NoopAssertionT, expectLanguages, restrictions.GetAllowedLanguages())
|
||||
})
|
||||
}
|
||||
|
||||
func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) {
|
||||
_, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()})
|
||||
require.NoError(t, err)
|
||||
awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer awaitCancel()
|
||||
await(t, awaitCtx, func() bool {
|
||||
defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{})
|
||||
return assert.NoError(NoopAssertionT, getErr) &&
|
||||
assert.Equal(NoopAssertionT, lang.String(), defaultLang.GetLanguage())
|
||||
})
|
||||
}
|
||||
|
||||
func importUser(ctx context.Context, preferredLanguage language.Tag) (*management.ImportHumanUserResponse, error) {
|
||||
random := integration.RandString(5)
|
||||
return Tester.Client.Mgmt.ImportHumanUser(ctx, &management.ImportHumanUserRequest{
|
||||
UserName: "integration-test-user_" + random,
|
||||
Profile: &management.ImportHumanUserRequest_Profile{
|
||||
FirstName: "hodor",
|
||||
LastName: "hodor",
|
||||
NickName: "hodor",
|
||||
PreferredLanguage: preferredLanguage.String(),
|
||||
},
|
||||
Email: &management.ImportHumanUserRequest_Email{
|
||||
Email: random + "@hodor.hodor",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
PasswordChangeRequired: false,
|
||||
Password: "Password1!",
|
||||
})
|
||||
}
|
||||
|
||||
func checkDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) {
|
||||
resp, err := http.Get("http://" + domain + ":8080/.well-known/openid-configuration")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
defer func() {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
}()
|
||||
require.NoError(t, err)
|
||||
doc := struct {
|
||||
UILocalesSupported []string `json:"ui_locales_supported"`
|
||||
}{}
|
||||
require.NoError(t, json.Unmarshal(body, &doc))
|
||||
if containsUILocales != nil {
|
||||
assert.Condition(NoopAssertionT, contains(doc.UILocalesSupported, containsUILocales))
|
||||
}
|
||||
if notContainsUILocales != nil {
|
||||
assert.Condition(NoopAssertionT, not(contains(doc.UILocalesSupported, notContainsUILocales)))
|
||||
}
|
||||
}
|
||||
|
||||
func checkLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://"+domain+":8080/ui/login/register", nil)
|
||||
req.Header.Set("Accept-Language", acceptLanguage.String())
|
||||
require.NoError(t, err)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
defer func() {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
}()
|
||||
require.NoError(t, err)
|
||||
assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String())
|
||||
}
|
||||
|
||||
// We would love to use assert.Contains here, but it doesn't work with slices of strings
|
||||
func contains(container []string, subset []string) assert.Comparison {
|
||||
return func() bool {
|
||||
if subset == nil {
|
||||
return true
|
||||
}
|
||||
for _, str := range subset {
|
||||
var found bool
|
||||
for _, containerStr := range container {
|
||||
if str == containerStr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func not(cmp assert.Comparison) assert.Comparison {
|
||||
return func() bool {
|
||||
return !cmp()
|
||||
}
|
||||
}
|
@@ -8,12 +8,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
)
|
||||
|
||||
var (
|
||||
AdminCTX, SystemCTX context.Context
|
||||
Tester *integration.Tester
|
||||
// NoopAssertionT is useful in combination with assert.Eventuallyf to use testify assertions in a callback
|
||||
NoopAssertionT = new(noopAssertionT)
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -30,3 +35,29 @@ func TestMain(m *testing.M) {
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func await(t *testing.T, ctx context.Context, cb func() bool) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok, "context must have deadline")
|
||||
assert.Eventuallyf(
|
||||
t,
|
||||
func() bool {
|
||||
defer func() {
|
||||
// Panics are not recovered and don't mark the test as failed, so we need to do that ourselves
|
||||
require.Nil(t, recover(), "panic in await callback")
|
||||
}()
|
||||
return cb()
|
||||
},
|
||||
time.Until(deadline),
|
||||
100*time.Millisecond,
|
||||
"awaiting successful callback failed",
|
||||
)
|
||||
}
|
||||
|
||||
var _ assert.TestingT = (*noopAssertionT)(nil)
|
||||
|
||||
type noopAssertionT struct{}
|
||||
|
||||
func (*noopAssertionT) FailNow() {}
|
||||
|
||||
func (*noopAssertionT) Errorf(string, ...interface{}) {}
|
||||
|
@@ -2,15 +2,12 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/text"
|
||||
auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
)
|
||||
|
||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
|
||||
langs, err := s.query.Languages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
|
||||
func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
|
||||
return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||
}
|
||||
|
@@ -2,15 +2,12 @@ package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/text"
|
||||
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
)
|
||||
|
||||
func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
|
||||
langs, err := s.query.Languages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
|
||||
func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
|
||||
return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
|
||||
}
|
||||
|
@@ -220,8 +220,7 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
|
||||
|
||||
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
||||
human := AddHumanUserRequestToAddHuman(req)
|
||||
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
|
||||
if err != nil {
|
||||
if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.AddHumanUserResponse{
|
||||
|
@@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) {
|
||||
// create unique names.
|
||||
lastName := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
userName := strings.Join([]string{firstName, lastName}, "_")
|
||||
email := strings.Join([]string{userName, "zitadel.com"}, "@")
|
||||
email := strings.Join([]string{userName, "example.com"}, "@")
|
||||
|
||||
res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
|
||||
UserName: userName,
|
||||
Profile: &management.ImportHumanUserRequest_Profile{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
PreferredLanguage: language.Afrikaans.String(),
|
||||
PreferredLanguage: language.Japanese.String(),
|
||||
Gender: user.Gender_GENDER_DIVERSE,
|
||||
},
|
||||
Email: &management.ImportHumanUserRequest_Email{
|
||||
@@ -82,3 +82,21 @@ func TestImport_and_Get(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImport_UnparsablePreferredLanguage(t *testing.T) {
|
||||
random := integration.RandString(5)
|
||||
_, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
|
||||
UserName: random,
|
||||
Profile: &management.ImportHumanUserRequest_Profile{
|
||||
FirstName: random,
|
||||
LastName: random,
|
||||
PreferredLanguage: "not valid",
|
||||
Gender: user.Gender_GENDER_DIVERSE,
|
||||
},
|
||||
Email: &management.ImportHumanUserRequest_Email{
|
||||
Email: random + "@example.com",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ const (
|
||||
)
|
||||
|
||||
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor {
|
||||
translator, err := newZitadelTranslator(language.English)
|
||||
translator, err := i18n.NewZitadelTranslator(language.English)
|
||||
logging.OnError(err).Panic("unable to get translator")
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...)
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
_ "github.com/zitadel/zitadel/internal/statik"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
@@ -18,17 +19,15 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if loc, ok := resp.(localizers); ok && resp != nil {
|
||||
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
|
||||
translator, translatorError := getTranslator(ctx)
|
||||
if translatorError != nil {
|
||||
logging.New().WithError(translatorError).Error("could not load translator")
|
||||
return resp, err
|
||||
}
|
||||
translateFields(ctx, loc, translator)
|
||||
}
|
||||
if err != nil {
|
||||
translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
|
||||
translator, translatorError := getTranslator(ctx)
|
||||
if translatorError != nil {
|
||||
logging.New().WithError(translatorError).Error("could not load translator")
|
||||
return resp, err
|
||||
}
|
||||
err = translateError(ctx, err, translator)
|
||||
@@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc.
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func getTranslator(ctx context.Context) (*i18n.Translator, error) {
|
||||
translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage())
|
||||
if err != nil {
|
||||
logging.New().WithError(err).Error("could not load translator")
|
||||
}
|
||||
return translator, err
|
||||
}
|
||||
|
@@ -4,10 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/rakyll/statik/fs"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
)
|
||||
@@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newZitadelTranslator(defaultLanguage language.Tag) (*i18n.Translator, error) {
|
||||
return translatorFromNamespace("zitadel", defaultLanguage)
|
||||
}
|
||||
|
||||
func translatorFromNamespace(namespace string, defaultLanguage language.Tag) (*i18n.Translator, error) {
|
||||
dir, err := fs.NewWithNamespace(namespace)
|
||||
logging.WithFields("namespace", namespace).OnError(err).Panic("unable to get namespace")
|
||||
|
||||
return i18n.NewTranslator(dir, defaultLanguage, "")
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/text"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
@@ -116,13 +117,9 @@ func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.G
|
||||
}
|
||||
|
||||
func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) {
|
||||
langs, err := s.query.Languages(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance := authz.GetInstance(ctx)
|
||||
return &settings.GetGeneralSettingsResponse{
|
||||
SupportedLanguages: text.LanguageTagsToStrings(langs),
|
||||
SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
|
||||
DefaultOrgId: instance.DefaultOrganisationID(),
|
||||
DefaultLanguage: instance.DefaultLanguage().String(),
|
||||
}, nil
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func LanguageTagsToStrings(langs []language.Tag) []string {
|
||||
result := make([]string, len(langs))
|
||||
for i, lang := range langs {
|
||||
result[i] = lang.String()
|
||||
}
|
||||
return result
|
||||
}
|
@@ -72,7 +72,7 @@ func MachineToPb(view *query.Machine) *user_pb.Machine {
|
||||
return &user_pb.Machine{
|
||||
Name: view.Name,
|
||||
Description: view.Description,
|
||||
HasSecret: view.HasSecret,
|
||||
HasSecret: view.Secret != nil,
|
||||
AccessTokenType: AccessTokenTypeToPb(view.AccessTokenType),
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,10 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We also need a user session
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RegisterPasskeyRequest
|
||||
@@ -95,14 +99,12 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||
https://github.com/zitadel/zitadel/issues/6022
|
||||
{
|
||||
name: "human user",
|
||||
name: "user setting its own passkey",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: humanUserID,
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
@@ -111,7 +113,6 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@@ -5,16 +5,22 @@ package user_test
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterTOTP(t *testing.T) {
|
||||
// userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -29,7 +35,7 @@ func TestServer_RegisterTOTP(t *testing.T) {
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.RegisterTOTPRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -37,19 +43,17 @@ func TestServer_RegisterTOTP(t *testing.T) {
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.RegisterTOTPRequest{
|
||||
UserId: "wrong",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||
https://github.com/zitadel/zitadel/issues/6022
|
||||
{
|
||||
name: "human user",
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.RegisterTOTPRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
@@ -60,7 +64,6 @@ func TestServer_RegisterTOTP(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -80,15 +83,16 @@ func TestServer_RegisterTOTP(t *testing.T) {
|
||||
|
||||
func TestServer_VerifyTOTPRegistration(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
|
||||
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||
reg, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{
|
||||
reg, err := Client.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
code, err := totp.GenerateCode(reg.Secret, time.Now())
|
||||
require.NoError(t, err)
|
||||
*/
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -103,7 +107,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: "wrong",
|
||||
},
|
||||
@@ -113,7 +117,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
|
||||
{
|
||||
name: "wrong code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: userID,
|
||||
Code: "123",
|
||||
@@ -121,12 +125,10 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||
https://github.com/zitadel/zitadel/issues/6022
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: userID,
|
||||
Code: code,
|
||||
@@ -138,7 +140,6 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@@ -11,12 +11,17 @@ import (
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterU2F(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
|
||||
// We also need a user session
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RegisterU2FRequest
|
||||
@@ -45,12 +50,10 @@ func TestServer_RegisterU2F(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||
https://github.com/zitadel/zitadel/issues/6022
|
||||
{
|
||||
name: "human user",
|
||||
name: "user setting its own passkey",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
|
||||
req: &user.RegisterU2FRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
@@ -61,7 +64,6 @@ func TestServer_RegisterU2F(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -85,8 +87,11 @@ func TestServer_RegisterU2F(t *testing.T) {
|
||||
|
||||
func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
/* TODO after we are able to obtain a Bearer token for a human user
|
||||
pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
|
||||
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -94,7 +99,6 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
*/
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -109,7 +113,7 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyU2FRegistrationRequest{
|
||||
U2FId: "123",
|
||||
TokenName: "nice name",
|
||||
@@ -117,11 +121,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO after we are able to obtain a Bearer token for a human user
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyU2FRegistrationRequest{
|
||||
UserId: userID,
|
||||
U2FId: pkr.GetU2FId(),
|
||||
@@ -135,11 +138,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
{
|
||||
name: "wrong credential",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
ctx: ctx,
|
||||
req: &user.VerifyU2FRegistrationRequest{
|
||||
UserId: userID,
|
||||
U2FId: "123",
|
||||
|
@@ -28,8 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
|
||||
return nil, err
|
||||
}
|
||||
orgID := authz.GetCtxData(ctx).OrgID
|
||||
err = s.command.AddHuman(ctx, orgID, human, false)
|
||||
if err != nil {
|
||||
if err = s.command.AddHuman(ctx, orgID, human, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddHumanUserResponse{
|
||||
|
@@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
parametersEqual: map[string]string{
|
||||
"client_id": "clientID",
|
||||
"prompt": "select_account",
|
||||
"redirect_uri": "http://localhost:8080/idps/callback",
|
||||
"redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback",
|
||||
"response_type": "code",
|
||||
"scope": "openid profile email",
|
||||
},
|
||||
@@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
url: "http://localhost:8000/sso",
|
||||
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
|
||||
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
url: "http://localhost:8000/sso",
|
||||
url: "http://" + Tester.Config.ExternalDomain + ":8000/sso",
|
||||
parametersExisting: []string{"RelayState", "SAMLRequest"},
|
||||
},
|
||||
wantErr: false,
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/rakyll/statik/fs"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@@ -112,7 +111,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host = u.Hostname()
|
||||
host = u.Host
|
||||
if host == "" {
|
||||
err = errors.New("empty host")
|
||||
}
|
||||
@@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) {
|
||||
}
|
||||
|
||||
func newZitadelTranslator() *i18n.Translator {
|
||||
dir, err := fs.NewWithNamespace("zitadel")
|
||||
logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace")
|
||||
|
||||
translator, err := i18n.NewTranslator(dir, language.English, "")
|
||||
translator, err := i18n.NewZitadelTranslator(language.English)
|
||||
logging.OnError(err).Panic("unable to get translator")
|
||||
return translator
|
||||
}
|
||||
|
@@ -221,7 +221,7 @@ func Test_setInstance(t *testing.T) {
|
||||
r.Header.Set("host", "fromrequest")
|
||||
return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999"))
|
||||
}(),
|
||||
verifier: &mockInstanceVerifier{"fromorigin"},
|
||||
verifier: &mockInstanceVerifier{"fromorigin:9999"},
|
||||
headerName: "host",
|
||||
},
|
||||
res{
|
||||
|
18
internal/api/http/middleware/middleware_test.go
Normal file
18
internal/api/http/middleware/middleware_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
SupportedLanguages = []language.Tag{language.English, language.German}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
i18n.SupportLanguages(SupportedLanguages...)
|
||||
m.Run()
|
||||
}
|
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
errz "github.com/zitadel/zitadel/internal/errors"
|
||||
zerrors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
)
|
||||
@@ -55,7 +55,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (*accessToke
|
||||
|
||||
token, err := s.repo.TokenByIDs(ctx, subject, tokenID)
|
||||
if err != nil {
|
||||
return nil, errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired")
|
||||
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired")
|
||||
}
|
||||
return accessTokenV1(tokenID, subject, token), nil
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke
|
||||
token.audience = append(token.audience, clientID)
|
||||
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
|
||||
if err != nil {
|
||||
return errz.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||
return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||
}
|
||||
roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
if err != nil {
|
||||
|
@@ -17,6 +17,7 @@ import (
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
)
|
||||
@@ -500,8 +501,7 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT
|
||||
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
|
||||
codeVerifier := "codeVerifier"
|
||||
return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(codeVerifier))
|
||||
return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
|
||||
}
|
||||
|
||||
func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
||||
|
@@ -43,32 +43,14 @@ const (
|
||||
func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
client, err := o.query.AppByOIDCClientID(ctx, id)
|
||||
client, err := o.query.GetOIDCClientByID(ctx, id, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client.State != domain.AppStateActive {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active")
|
||||
}
|
||||
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(client.ProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal")
|
||||
}
|
||||
projectRoles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedScopes := make([]string, len(projectRoles.ProjectRoles))
|
||||
for i, role := range projectRoles.ProjectRoles {
|
||||
allowedScopes[i] = ScopeProjectRolePrefix + role.Key
|
||||
}
|
||||
|
||||
accessTokenLifetime, idTokenLifetime, _, _, err := o.getOIDCSettings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2, accessTokenLifetime, idTokenLifetime, allowedScopes)
|
||||
return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) {
|
||||
@@ -235,22 +217,10 @@ func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
|
||||
loginname, err := query.NewUserLoginNamesSearchQuery(clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := o.query.GetUser(ctx, false, loginname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientCredentialsClient{
|
||||
id: clientID,
|
||||
tokenType: accessTokenTypeToOIDC(user.Machine.AccessTokenType),
|
||||
}, nil
|
||||
// ClientCredentials method is kept to keep the storage interface implemented.
|
||||
// However, it should never be called as the VerifyClient method on the Server is overridden.
|
||||
func (o *OPStorage) ClientCredentials(context.Context, string, string) (op.Client, error) {
|
||||
return nil, errors.ThrowInternal(nil, "OIDC-Su8So", "Errors.Internal")
|
||||
}
|
||||
|
||||
// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
|
||||
@@ -934,3 +904,67 @@ func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interf
|
||||
return c.Runtime.ToValue(claims)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials {
|
||||
return s.clientCredentialsAuth(ctx, r.Data.ClientID, r.Data.ClientSecret)
|
||||
}
|
||||
|
||||
clientID, assertion, err := clientIDFromCredentials(r.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion)
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err // defaults to server error
|
||||
}
|
||||
if client.State != domain.AppStateActive {
|
||||
return nil, oidc.ErrInvalidClient().WithDescription("client is not active")
|
||||
}
|
||||
|
||||
switch client.AuthMethodType {
|
||||
case domain.OIDCAuthMethodTypeBasic, domain.OIDCAuthMethodTypePost:
|
||||
err = s.verifyClientSecret(ctx, client, r.Data.ClientSecret)
|
||||
case domain.OIDCAuthMethodTypePrivateKeyJWT:
|
||||
err = s.verifyClientAssertion(ctx, client, r.Data.ClientAssertion)
|
||||
case domain.OIDCAuthMethodTypeNone:
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ClientFromBusiness(client, s.defaultLoginURL, s.defaultLoginURLV2), nil
|
||||
}
|
||||
|
||||
func (s *Server) verifyClientAssertion(ctx context.Context, client *query.OIDCClient, assertion string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if assertion == "" {
|
||||
return oidc.ErrInvalidClient().WithDescription("empty client assertion")
|
||||
}
|
||||
verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.ClockSkew)
|
||||
if _, err := op.VerifyJWTAssertion(ctx, assertion, verifier); err != nil {
|
||||
return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid assertion")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) verifyClientSecret(ctx context.Context, client *query.OIDCClient, secret string) (err error) {
|
||||
_, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if secret == "" {
|
||||
return oidc.ErrInvalidClient().WithDescription("empty client secret")
|
||||
}
|
||||
if err = crypto.CompareHash(client.ClientSecret, []byte(secret), s.hashAlg); err != nil {
|
||||
return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid secret")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -9,43 +10,40 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
app *query.App
|
||||
defaultLoginURL string
|
||||
defaultLoginURLV2 string
|
||||
defaultAccessTokenLifetime time.Duration
|
||||
defaultIdTokenLifetime time.Duration
|
||||
allowedScopes []string
|
||||
client *query.OIDCClient
|
||||
defaultLoginURL string
|
||||
defaultLoginURLV2 string
|
||||
allowedScopes []string
|
||||
}
|
||||
|
||||
func ClientFromBusiness(app *query.App, defaultLoginURL, defaultLoginURLV2 string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) {
|
||||
if app.OIDCConfig == nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application")
|
||||
func ClientFromBusiness(client *query.OIDCClient, defaultLoginURL, defaultLoginURLV2 string) op.Client {
|
||||
allowedScopes := make([]string, len(client.ProjectRoleKeys))
|
||||
for i, roleKey := range client.ProjectRoleKeys {
|
||||
allowedScopes[i] = ScopeProjectRolePrefix + roleKey
|
||||
}
|
||||
|
||||
return &Client{
|
||||
app: app,
|
||||
defaultLoginURL: defaultLoginURL,
|
||||
defaultLoginURLV2: defaultLoginURLV2,
|
||||
defaultAccessTokenLifetime: defaultAccessTokenLifetime,
|
||||
defaultIdTokenLifetime: defaultIdTokenLifetime,
|
||||
allowedScopes: allowedScopes},
|
||||
nil
|
||||
client: client,
|
||||
defaultLoginURL: defaultLoginURL,
|
||||
defaultLoginURLV2: defaultLoginURLV2,
|
||||
allowedScopes: allowedScopes,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ApplicationType() op.ApplicationType {
|
||||
return op.ApplicationType(c.app.OIDCConfig.AppType)
|
||||
return op.ApplicationType(c.client.ApplicationType)
|
||||
}
|
||||
|
||||
func (c *Client) AuthMethod() oidc.AuthMethod {
|
||||
return authMethodToOIDC(c.app.OIDCConfig.AuthMethodType)
|
||||
return authMethodToOIDC(c.client.AuthMethodType)
|
||||
}
|
||||
|
||||
func (c *Client) GetID() string {
|
||||
return c.app.OIDCConfig.ClientID
|
||||
return c.client.ClientID
|
||||
}
|
||||
|
||||
func (c *Client) LoginURL(id string) string {
|
||||
@@ -56,28 +54,28 @@ func (c *Client) LoginURL(id string) string {
|
||||
}
|
||||
|
||||
func (c *Client) RedirectURIs() []string {
|
||||
return c.app.OIDCConfig.RedirectURIs
|
||||
return c.client.RedirectURIs
|
||||
}
|
||||
|
||||
func (c *Client) PostLogoutRedirectURIs() []string {
|
||||
return c.app.OIDCConfig.PostLogoutRedirectURIs
|
||||
return c.client.PostLogoutRedirectURIs
|
||||
}
|
||||
|
||||
func (c *Client) ResponseTypes() []oidc.ResponseType {
|
||||
return responseTypesToOIDC(c.app.OIDCConfig.ResponseTypes)
|
||||
return responseTypesToOIDC(c.client.ResponseTypes)
|
||||
}
|
||||
|
||||
func (c *Client) GrantTypes() []oidc.GrantType {
|
||||
return grantTypesToOIDC(c.app.OIDCConfig.GrantTypes)
|
||||
return grantTypesToOIDC(c.client.GrantTypes)
|
||||
}
|
||||
|
||||
func (c *Client) DevMode() bool {
|
||||
return c.app.OIDCConfig.IsDevMode
|
||||
return c.client.IsDevMode
|
||||
}
|
||||
|
||||
func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
|
||||
return func(scopes []string) []string {
|
||||
if c.app.OIDCConfig.AssertIDTokenRole {
|
||||
if c.client.IDTokenRoleAssertion {
|
||||
return scopes
|
||||
}
|
||||
return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix)
|
||||
@@ -86,7 +84,7 @@ func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []strin
|
||||
|
||||
func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
|
||||
return func(scopes []string) []string {
|
||||
if c.app.OIDCConfig.AssertAccessTokenRole {
|
||||
if c.client.AccessTokenRoleAssertion {
|
||||
return scopes
|
||||
}
|
||||
return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix)
|
||||
@@ -94,15 +92,15 @@ func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []s
|
||||
}
|
||||
|
||||
func (c *Client) AccessTokenLifetime() time.Duration {
|
||||
return c.defaultAccessTokenLifetime //PLANNED: impl from real client
|
||||
return c.client.AccessTokenLifetime
|
||||
}
|
||||
|
||||
func (c *Client) IDTokenLifetime() time.Duration {
|
||||
return c.defaultIdTokenLifetime //PLANNED: impl from real client
|
||||
return c.client.IDTokenLifetime
|
||||
}
|
||||
|
||||
func (c *Client) AccessTokenType() op.AccessTokenType {
|
||||
return accessTokenTypeToOIDC(c.app.OIDCConfig.AccessTokenType)
|
||||
return accessTokenTypeToOIDC(c.client.AccessTokenType)
|
||||
}
|
||||
|
||||
func (c *Client) IsScopeAllowed(scope string) bool {
|
||||
@@ -127,20 +125,15 @@ func (c *Client) IsScopeAllowed(scope string) bool {
|
||||
if scope == ScopeProjectsRoles {
|
||||
return true
|
||||
}
|
||||
for _, allowedScope := range c.allowedScopes {
|
||||
if scope == allowedScope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(c.allowedScopes, scope)
|
||||
}
|
||||
|
||||
func (c *Client) ClockSkew() time.Duration {
|
||||
return c.app.OIDCConfig.ClockSkew
|
||||
return c.client.ClockSkew
|
||||
}
|
||||
|
||||
func (c *Client) IDTokenUserinfoClaimsAssertion() bool {
|
||||
return c.app.OIDCConfig.AssertIDTokenUserinfo
|
||||
return c.client.IDTokenUserinfoAssertion
|
||||
}
|
||||
|
||||
func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType {
|
||||
@@ -229,3 +222,14 @@ func removeScopeWithPrefix(scopes []string, scopePrefix ...string) []string {
|
||||
}
|
||||
return newScopeList
|
||||
}
|
||||
|
||||
func clientIDFromCredentials(cc *op.ClientCredentials) (clientID string, assertion bool, err error) {
|
||||
if cc.ClientAssertion != "" {
|
||||
claims := new(oidc.JWTTokenRequest)
|
||||
if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil {
|
||||
return "", false, oidc.ErrInvalidClient().WithParent(err)
|
||||
}
|
||||
return claims.Issuer, true, nil
|
||||
}
|
||||
return cc.ClientID, false, nil
|
||||
}
|
||||
|
@@ -1,10 +1,15 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type clientCredentialsRequest struct {
|
||||
@@ -28,15 +33,42 @@ func (c *clientCredentialsRequest) GetScopes() []string {
|
||||
return c.scopes
|
||||
}
|
||||
|
||||
func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
|
||||
searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := s.query.GetUser(ctx, false, searchQuery)
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err // defaults to server error
|
||||
}
|
||||
if user.Machine == nil || user.Machine.Secret == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-pieP8", "Errors.User.Machine.Secret.NotExisting")
|
||||
}
|
||||
if err = crypto.CompareHash(user.Machine.Secret, []byte(clientSecret), s.hashAlg); err != nil {
|
||||
s.command.MachineSecretCheckFailed(ctx, user.ID, user.ResourceOwner)
|
||||
return nil, errors.ThrowInvalidArgument(err, "OIDC-VoXo6", "Errors.User.Machine.Secret.Invalid")
|
||||
}
|
||||
|
||||
s.command.MachineSecretCheckSucceeded(ctx, user.ID, user.ResourceOwner)
|
||||
return &clientCredentialsClient{
|
||||
id: clientID,
|
||||
user: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type clientCredentialsClient struct {
|
||||
id string
|
||||
tokenType op.AccessTokenType
|
||||
id string
|
||||
user *query.User
|
||||
}
|
||||
|
||||
// AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request
|
||||
// machine users currently only have opaque tokens ([op.AccessTokenTypeBearer])
|
||||
func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType {
|
||||
return c.tokenType
|
||||
return accessTokenTypeToOIDC(c.user.Machine.AccessTokenType)
|
||||
}
|
||||
|
||||
// GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request
|
||||
|
@@ -4,20 +4,26 @@ package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/authn"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user"
|
||||
)
|
||||
|
||||
func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
|
||||
@@ -142,3 +148,245 @@ func assertIntrospection(
|
||||
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"])
|
||||
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"])
|
||||
}
|
||||
|
||||
// TestServer_VerifyClient tests verification by running code flow tests
|
||||
// with clients that have different authentication methods.
|
||||
func TestServer_VerifyClient(t *testing.T) {
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
|
||||
inactiveClient, err := Tester.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
nativeClient, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
basicWebClient, err := Tester.CreateOIDCWebClientBasic(CTX, redirectURI, logoutRedirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
jwtWebClient, keyData, err := Tester.CreateOIDCWebClientJWT(CTX, redirectURI, logoutRedirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
|
||||
type clientDetails struct {
|
||||
authReqClientID string
|
||||
clientID string
|
||||
clientSecret string
|
||||
keyData []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
client clientDetails
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty client ID error",
|
||||
client: clientDetails{
|
||||
authReqClientID: nativeClient.GetClientId(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "client not found error",
|
||||
client: clientDetails{
|
||||
authReqClientID: nativeClient.GetClientId(),
|
||||
clientID: "foo",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "client inactive error",
|
||||
client: clientDetails{
|
||||
authReqClientID: nativeClient.GetClientId(),
|
||||
clientID: inactiveClient.GetClientId(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "native client success",
|
||||
client: clientDetails{
|
||||
authReqClientID: nativeClient.GetClientId(),
|
||||
clientID: nativeClient.GetClientId(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "web client basic secret empty error",
|
||||
client: clientDetails{
|
||||
authReqClientID: basicWebClient.GetClientId(),
|
||||
clientID: basicWebClient.GetClientId(),
|
||||
clientSecret: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "web client basic secret invalid error",
|
||||
client: clientDetails{
|
||||
authReqClientID: basicWebClient.GetClientId(),
|
||||
clientID: basicWebClient.GetClientId(),
|
||||
clientSecret: "wrong",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "web client basic secret success",
|
||||
client: clientDetails{
|
||||
authReqClientID: basicWebClient.GetClientId(),
|
||||
clientID: basicWebClient.GetClientId(),
|
||||
clientSecret: basicWebClient.GetClientSecret(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "web client JWT profile empty assertion error",
|
||||
client: clientDetails{
|
||||
authReqClientID: jwtWebClient.GetClientId(),
|
||||
clientID: jwtWebClient.GetClientId(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "web client JWT profile invalid assertion error",
|
||||
client: clientDetails{
|
||||
authReqClientID: jwtWebClient.GetClientId(),
|
||||
clientID: jwtWebClient.GetClientId(),
|
||||
keyData: createInvalidKeyData(t, jwtWebClient),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "web client JWT profile success",
|
||||
client: clientDetails{
|
||||
authReqClientID: jwtWebClient.GetClientId(),
|
||||
clientID: jwtWebClient.GetClientId(),
|
||||
keyData: keyData,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fmt.Printf("\n\n%s\n\n", tt.client.keyData)
|
||||
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, oidc.ScopeOpenID)
|
||||
require.NoError(t, err)
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// use a new RP so we can inject different credentials
|
||||
var options []rp.Option
|
||||
if tt.client.keyData != nil {
|
||||
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyFile(tt.client.keyData)))
|
||||
}
|
||||
provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.client.clientID, tt.client.clientSecret, redirectURI, []string{oidc.ScopeOpenID}, options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
// test code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
codeOpts := codeExchangeOptions(t, provider)
|
||||
tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, codeOpts...)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, false)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func codeExchangeOptions(t testing.TB, provider rp.RelyingParty) []rp.CodeExchangeOpt {
|
||||
codeOpts := []rp.CodeExchangeOpt{rp.WithCodeVerifier(integration.CodeVerifier)}
|
||||
if signer := provider.Signer(); signer != nil {
|
||||
assertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer())
|
||||
require.NoError(t, err)
|
||||
codeOpts = append(codeOpts, rp.WithClientAssertionJWT(assertion))
|
||||
}
|
||||
return codeOpts
|
||||
}
|
||||
|
||||
func createInvalidKeyData(t testing.TB, client *management.AddOIDCAppResponse) []byte {
|
||||
key := domain.ApplicationKey{
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
KeyID: "1",
|
||||
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"),
|
||||
ApplicationID: client.GetAppId(),
|
||||
ClientID: client.GetClientId(),
|
||||
}
|
||||
data, err := key.Detail()
|
||||
require.NoError(t, err)
|
||||
return data
|
||||
}
|
||||
|
||||
func TestServer_CreateAccessToken_ClientCredentials(t *testing.T) {
|
||||
clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
|
||||
require.NoError(t, err)
|
||||
|
||||
type clientDetails struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
keyData []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
clientID string
|
||||
clientSecret string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing client ID error",
|
||||
clientID: "",
|
||||
clientSecret: clientSecret,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "client not found error",
|
||||
clientID: "foo",
|
||||
clientSecret: clientSecret,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "machine user without secret error",
|
||||
clientID: func() string {
|
||||
name := gofakeit.Username()
|
||||
_, err := Tester.Client.Mgmt.AddMachineUser(CTX, &management.AddMachineUserRequest{
|
||||
Name: name,
|
||||
UserName: name,
|
||||
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return name
|
||||
}(),
|
||||
clientSecret: clientSecret,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong secret error",
|
||||
clientID: clientID,
|
||||
clientSecret: "bar",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.clientID, tt.clientSecret, redirectURI, []string{oidc.ScopeOpenID})
|
||||
require.NoError(t, err)
|
||||
tokens, err := rp.ClientCredentials(CTX, provider, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tokens)
|
||||
assert.NotEmpty(t, tokens.AccessToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
errz "github.com/zitadel/zitadel/internal/errors"
|
||||
zerrors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
@@ -31,14 +31,14 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
clientChan := make(chan *instrospectionClientResult)
|
||||
go s.instrospectionClientAuth(ctx, r.Data.ClientCredentials, clientChan)
|
||||
clientChan := make(chan *introspectionClientResult)
|
||||
go s.introspectionClientAuth(ctx, r.Data.ClientCredentials, clientChan)
|
||||
|
||||
tokenChan := make(chan *introspectionTokenResult)
|
||||
go s.introspectionToken(ctx, r.Data.Token, tokenChan)
|
||||
|
||||
var (
|
||||
client *instrospectionClientResult
|
||||
client *introspectionClientResult
|
||||
token *introspectionTokenResult
|
||||
)
|
||||
|
||||
@@ -116,13 +116,13 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
|
||||
return op.NewResponse(introspectionResp), nil
|
||||
}
|
||||
|
||||
type instrospectionClientResult struct {
|
||||
type introspectionClientResult struct {
|
||||
clientID string
|
||||
projectID string
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) {
|
||||
func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *introspectionClientResult) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
|
||||
clientID, projectID, err := func() (string, string, error) {
|
||||
@@ -147,7 +147,7 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred
|
||||
|
||||
span.EndWithError(err)
|
||||
|
||||
rc <- &instrospectionClientResult{
|
||||
rc <- &introspectionClientResult{
|
||||
clientID: clientID,
|
||||
projectID: projectID,
|
||||
err: err,
|
||||
@@ -157,15 +157,11 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred
|
||||
// clientFromCredentials parses the client ID early,
|
||||
// and makes a single query for the client for either auth methods.
|
||||
func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) {
|
||||
if cc.ClientAssertion != "" {
|
||||
claims := new(oidc.JWTTokenRequest)
|
||||
if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil {
|
||||
return nil, oidc.ErrUnauthorizedClient().WithParent(err)
|
||||
}
|
||||
client, err = s.query.GetIntrospectionClientByID(ctx, claims.Issuer, true)
|
||||
} else {
|
||||
client, err = s.query.GetIntrospectionClientByID(ctx, cc.ClientID, false)
|
||||
clientID, assertion, err := clientIDFromCredentials(cc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, oidc.ErrUnauthorizedClient().WithParent(err)
|
||||
}
|
||||
@@ -196,5 +192,5 @@ func validateIntrospectionAudience(audience []string, clientID, projectID string
|
||||
return nil
|
||||
}
|
||||
|
||||
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||
return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||
}
|
||||
|
@@ -31,9 +31,9 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
redirectURI = "oidcintegrationtest://callback"
|
||||
redirectURI = "https://callback"
|
||||
redirectURIImplicit = "http://localhost:9999/callback"
|
||||
logoutRedirectURI = "oidcintegrationtest://logged-out"
|
||||
logoutRedirectURI = "https://logged-out"
|
||||
zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
|
||||
)
|
||||
|
||||
|
@@ -6,11 +6,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rakyll/statik/fs"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"golang.org/x/exp/slog"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/assets"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
@@ -23,7 +21,6 @@ import (
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
)
|
||||
@@ -131,6 +128,9 @@ func NewServer(
|
||||
query: query,
|
||||
command: command,
|
||||
keySet: newKeySet(context.TODO(), time.Hour, query.GetActivePublicKeyByID),
|
||||
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||
defaultLoginURLV2: config.DefaultLoginURLV2,
|
||||
defaultLogoutURLV2: config.DefaultLogoutURLV2,
|
||||
fallbackLogger: fallbackLogger,
|
||||
hashAlg: crypto.NewBCrypt(10), // as we are only verifying in oidc, the cost is already part of the hash string and the config here is irrelevant.
|
||||
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
||||
@@ -167,10 +167,6 @@ func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string {
|
||||
}
|
||||
|
||||
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
|
||||
supportedLanguages, err := getSupportedLanguages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opConfig := &op.Config{
|
||||
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
||||
CodeMethodS256: config.CodeMethodS256,
|
||||
@@ -178,7 +174,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
|
||||
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
|
||||
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
||||
RequestObjectSupported: config.RequestObjectSupported,
|
||||
SupportedUILocales: supportedLanguages,
|
||||
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
|
||||
}
|
||||
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
|
||||
@@ -211,11 +206,3 @@ func newStorage(config Config, command *command.Commands, query *query.Queries,
|
||||
func (o *OPStorage) Health(ctx context.Context) error {
|
||||
return o.repo.Health(ctx)
|
||||
}
|
||||
|
||||
func getSupportedLanguages() ([]language.Tag, error) {
|
||||
statikLoginFS, err := fs.NewWithNamespace("login")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i18n.SupportedLanguages(statikLoginFS)
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
@@ -26,6 +27,10 @@ type Server struct {
|
||||
command *command.Commands
|
||||
keySet *keySetCache
|
||||
|
||||
defaultLoginURL string
|
||||
defaultLoginURLV2 string
|
||||
defaultLogoutURLV2 string
|
||||
|
||||
fallbackLogger *slog.Logger
|
||||
hashAlg crypto.HashAlgorithm
|
||||
signingKeyAlgorithm string
|
||||
@@ -103,8 +108,15 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp
|
||||
func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
return op.NewResponse(s.createDiscoveryConfig(ctx)), nil
|
||||
restrictions, err := s.query.GetInstanceRestrictions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedLanguages := restrictions.AllowedLanguages
|
||||
if len(allowedLanguages) == 0 {
|
||||
allowedLanguages = i18n.SupportedLanguages()
|
||||
}
|
||||
return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil
|
||||
}
|
||||
|
||||
func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) {
|
||||
@@ -135,13 +147,6 @@ func (s *Server) DeviceAuthorization(ctx context.Context, r *op.ClientRequest[oi
|
||||
return s.LegacyServer.DeviceAuthorization(ctx, r)
|
||||
}
|
||||
|
||||
func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
return s.LegacyServer.VerifyClient(ctx, r)
|
||||
}
|
||||
|
||||
func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
@@ -205,7 +210,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe
|
||||
return s.LegacyServer.EndSession(ctx, r)
|
||||
}
|
||||
|
||||
func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfiguration {
|
||||
func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration {
|
||||
issuer := op.IssuerFromContext(ctx)
|
||||
return &oidc.DiscoveryConfiguration{
|
||||
Issuer: issuer,
|
||||
@@ -231,7 +236,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi
|
||||
RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()),
|
||||
ClaimsSupported: op.SupportedClaims(s.Provider()),
|
||||
CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()),
|
||||
UILocalesSupported: s.Provider().SupportedUILocales(),
|
||||
UILocalesSupported: supportedUILocales,
|
||||
RequestParameterSupported: s.Provider().RequestObjectSupported(),
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
||||
signingKeyAlgorithm string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
supportedUILocales []language.Tag
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
||||
AuthMethodPrivateKeyJWT: true,
|
||||
GrantTypeRefreshToken: true,
|
||||
RequestObjectSupported: true,
|
||||
SupportedUILocales: []language.Tag{language.English, language.German},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
@@ -56,7 +56,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
||||
signingKeyAlgorithm: "RS256",
|
||||
},
|
||||
args{
|
||||
ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"),
|
||||
ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"),
|
||||
supportedUILocales: []language.Tag{language.English, language.German},
|
||||
},
|
||||
&oidc.DiscoveryConfiguration{
|
||||
Issuer: "https://issuer.com",
|
||||
@@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
|
||||
LegacyServer: tt.fields.LegacyServer,
|
||||
signingKeyAlgorithm: tt.fields.signingKeyAlgorithm,
|
||||
}
|
||||
assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx), "createDiscoveryConfig(%v)", tt.args.ctx)
|
||||
assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx, tt.args.supportedUILocales), "createDiscoveryConfig(%v)", tt.args.ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -36,13 +36,13 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
|
||||
var errID, errMessage string
|
||||
var errType, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
errType, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := passwordData{
|
||||
baseData: l.getBaseData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage),
|
||||
profileData: l.getProfileData(authReq),
|
||||
}
|
||||
policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID)
|
||||
@@ -65,8 +65,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut
|
||||
}
|
||||
|
||||
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
var errType, errMessage string
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage)
|
||||
data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "")
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil)
|
||||
}
|
||||
|
@@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request,
|
||||
logging.WithError(err).Error()
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
|
||||
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), nil)
|
||||
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &struct {
|
||||
baseData
|
||||
AuthRequestID string
|
||||
@@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a
|
||||
ClientID string
|
||||
Scopes []string
|
||||
}{
|
||||
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
|
||||
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
|
||||
AuthRequestID: authReq.ID,
|
||||
Username: authReq.UserName,
|
||||
ClientID: authReq.ApplicationID,
|
||||
Scopes: scopes,
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
|
||||
}
|
||||
|
||||
@@ -60,14 +59,13 @@ const (
|
||||
|
||||
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
|
||||
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &struct {
|
||||
baseData
|
||||
Message string
|
||||
}{
|
||||
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
|
||||
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
switch action {
|
||||
case deviceAuthAllowed:
|
||||
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
|
||||
|
@@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := externalNotFoundOptionData{
|
||||
baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
|
||||
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
|
||||
externalRegisterFormData: externalRegisterFormData{
|
||||
Email: human.EmailAddress,
|
||||
|
@@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
|
||||
data := initPasswordData{
|
||||
baseData: l.getBaseData(r, authReq, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
|
||||
profileData: l.getProfileData(authReq),
|
||||
UserID: userID,
|
||||
Code: code,
|
||||
@@ -153,8 +153,8 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
|
||||
}
|
||||
|
||||
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
|
||||
data := l.getUserData(r, authReq, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
|
||||
if authReq == nil {
|
||||
l.customTexts(r.Context(), translator, orgID)
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := initUserData{
|
||||
baseData: l.getBaseData(r, authReq, "InitUser.Title", "InitUser.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage),
|
||||
profileData: l.getProfileData(authReq),
|
||||
UserID: userID,
|
||||
Code: code,
|
||||
@@ -155,8 +155,8 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
|
||||
}
|
||||
|
||||
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
|
||||
data := l.getUserData(r, authReq, "InitUserDone.Title", "InitUserDone.Description", "", "")
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "")
|
||||
if authReq == nil {
|
||||
l.customTexts(r.Context(), translator, orgID)
|
||||
}
|
||||
|
@@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
temp := l.renderer.Templates[tmplLDAPLogin]
|
||||
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, translator, temp, data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -19,6 +19,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
|
||||
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
|
||||
var errType, errMessage string
|
||||
data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil)
|
||||
}
|
||||
|
@@ -2,15 +2,12 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
@@ -93,17 +90,12 @@ func CreateLogin(config Config,
|
||||
userCodeAlg: userCodeAlg,
|
||||
featureCheck: featureCheck,
|
||||
}
|
||||
statikFS, err := fs.NewWithNamespace("login")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create filesystem: %w", err)
|
||||
}
|
||||
|
||||
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
|
||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
||||
|
||||
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
|
||||
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
|
||||
login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
|
||||
login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName)
|
||||
login.parser = form.NewParser()
|
||||
return login, nil
|
||||
}
|
||||
|
@@ -99,7 +99,8 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
|
||||
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
|
||||
return
|
||||
}
|
||||
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
|
||||
funcs := map[string]interface{}{
|
||||
"hasUsernamePasswordLogin": func() bool {
|
||||
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword
|
||||
@@ -111,7 +112,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom
|
||||
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister
|
||||
},
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogin], data, funcs)
|
||||
}
|
||||
|
||||
func singleIDPAllowed(authReq *domain.AuthRequest) bool {
|
||||
|
@@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := loginSuccessData{
|
||||
userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage),
|
||||
}
|
||||
if authReq != nil {
|
||||
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)
|
||||
@@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
||||
return
|
||||
}
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLoginSuccess], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
|
@@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
|
||||
data := l.getUserData(r, nil, "LogoutDone.Title", "LogoutDone.Description", "", "")
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil)
|
||||
translator := l.getTranslator(r.Context(), nil)
|
||||
data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "")
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil)
|
||||
}
|
||||
|
@@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := mailVerificationData{
|
||||
baseData: l.getBaseData(r, authReq, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
|
||||
UserID: userID,
|
||||
profileData: l.getProfileData(authReq),
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
|
||||
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := mailVerificationData{
|
||||
baseData: l.getBaseData(r, authReq, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
|
||||
baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
|
||||
profileData: l.getProfileData(authReq),
|
||||
}
|
||||
if authReq == nil {
|
||||
|
@@ -16,7 +16,7 @@ type mfaInitDoneData struct {
|
||||
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
|
||||
var errType, errMessage string
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data.baseData = l.getBaseData(r, authReq, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
|
||||
data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
|
||||
data.profileData = l.getProfileData(authReq)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
|
||||
}
|
||||
|
@@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
|
||||
data.profileData = l.getProfileData(authReq)
|
||||
data.MFAType = domain.MFATypeOTPSMS
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil)
|
||||
}
|
||||
|
||||
// handleRegisterSMSCheck handles form submissions of the SMS registration.
|
||||
|
@@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe
|
||||
if u2f != nil {
|
||||
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &u2fInitData{
|
||||
webAuthNData: webAuthNData{
|
||||
userData: l.getUserData(r, authReq, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
|
||||
CredentialCreationData: credentialData,
|
||||
},
|
||||
MFAType: domain.MFATypeU2F,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAU2FInit], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
|
||||
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
|
||||
data.profileData = l.getProfileData(authReq)
|
||||
if data.MFAType == domain.MFATypeTOTP {
|
||||
code, err := generateQrCode(data.totpData.Url)
|
||||
|
@@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := mfaData{
|
||||
baseData: l.getBaseData(r, authReq, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
|
||||
profileData: l.getProfileData(authReq),
|
||||
}
|
||||
|
||||
|
@@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getUserData(r, authReq, "", "", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "", "", errID, errMessage)
|
||||
if verificationStep == nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
|
||||
switch selectedProvider {
|
||||
case domain.MFATypeU2F:
|
||||
|
@@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &mfaOTPData{
|
||||
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
|
||||
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
|
||||
SelectedProvider: selectedProvider,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplOTPVerification], data, nil)
|
||||
}
|
||||
|
||||
// handleOTPVerificationCheck handles form submissions of the OTP verification.
|
||||
|
@@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au
|
||||
if webAuthNLogin != nil {
|
||||
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &mfaU2FData{
|
||||
webAuthNData: webAuthNData{
|
||||
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
|
||||
CredentialCreationData: credentialData,
|
||||
},
|
||||
MFAProviders: providers,
|
||||
SelectedProvider: -1,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplU2FVerification], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplU2FVerification], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getUserData(r, authReq, "Password.Title", "Password.Description", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage)
|
||||
funcs := map[string]interface{}{
|
||||
"showPasswordReset": func() bool {
|
||||
if authReq.LoginPolicy != nil {
|
||||
@@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
|
||||
return true
|
||||
},
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPassword], data, funcs)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPassword], data, funcs)
|
||||
}
|
||||
|
||||
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request,
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil)
|
||||
}
|
||||
|
@@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
|
||||
if passwordSet && authReq.LoginPolicy != nil {
|
||||
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &passwordlessData{
|
||||
webAuthNData{
|
||||
userData: l.getUserData(r, authReq, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
|
||||
CredentialCreationData: credentialData,
|
||||
},
|
||||
passwordSet,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessVerification], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request,
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := &passwordlessPromptData{
|
||||
userData: l.getUserData(r, authReq, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &passwordlessPromptData{
|
||||
userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
|
||||
}
|
||||
|
@@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
|
||||
if webAuthNToken != nil {
|
||||
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := &passwordlessRegistrationData{
|
||||
webAuthNData{
|
||||
userData: l.getUserData(r, authReq, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
|
||||
CredentialCreationData: credentialData,
|
||||
},
|
||||
code,
|
||||
@@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
|
||||
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
|
||||
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
|
||||
data.LabelPolicy = labelPolicyToDomain(policy)
|
||||
|
||||
translator, err = l.renderer.NewTranslator(r.Context())
|
||||
if err == nil {
|
||||
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
|
||||
logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
|
||||
@@ -193,9 +190,8 @@ func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *htt
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
|
||||
data := passwordlessRegistrationDoneDate{
|
||||
userData: l.getUserData(r, authReq, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
|
||||
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
|
||||
HideNextButton: authReq == nil,
|
||||
}
|
||||
if authReq == nil {
|
||||
|
@@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
|
||||
l.renderRegister(w, r, authRequest, data, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
|
||||
if err != nil {
|
||||
l.renderRegister(w, r, authRequest, data, err)
|
||||
@@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque
|
||||
}
|
||||
|
||||
data := registerData{
|
||||
baseData: l.getBaseData(r, authRequest, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
|
||||
registerFormData: *formData,
|
||||
}
|
||||
|
||||
|
@@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := registerOptionData{
|
||||
baseData: l.getBaseData(r, authReq, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"hasRegistration": func() bool {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@@ -39,8 +38,12 @@ type registerOrgData struct {
|
||||
}
|
||||
|
||||
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
||||
if disallowed || err != nil {
|
||||
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
if restrictions.DisallowPublicOrgRegistration {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -54,8 +57,12 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
|
||||
disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context())
|
||||
if disallowed || err != nil {
|
||||
restrictions, err := l.query.GetInstanceRestrictions(r.Context())
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
if restrictions.DisallowPublicOrgRegistration {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
@@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
|
||||
}
|
||||
translator := l.getTranslator(r.Context(), authRequest)
|
||||
data := registerOrgData{
|
||||
baseData: l.getBaseData(r, authRequest, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
|
||||
baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
|
||||
registerOrgFormData: *formData,
|
||||
}
|
||||
pwPolicy := l.getPasswordComplexityPolicy(r, "0")
|
||||
@@ -130,11 +137,6 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) {
|
||||
restrictions, err := l.query.GetInstanceRestrictions(ctx)
|
||||
return restrictions.DisallowPublicOrgRegistration, err
|
||||
}
|
||||
|
||||
func (d registerOrgFormData) toUserDomain() *domain.Human {
|
||||
if d.Username == "" {
|
||||
d.Username = string(d.Email)
|
||||
|
@@ -39,7 +39,7 @@ type LanguageData struct {
|
||||
Lang string
|
||||
}
|
||||
|
||||
func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string) *Renderer {
|
||||
func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName string) *Renderer {
|
||||
r := &Renderer{
|
||||
pathPrefix: pathPrefix,
|
||||
staticStorage: staticStorage,
|
||||
@@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
}
|
||||
var err error
|
||||
r.Renderer, err = renderer.NewRenderer(
|
||||
staticDir,
|
||||
tmplMapping, funcs,
|
||||
cookieName,
|
||||
)
|
||||
@@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
|
||||
|
||||
_, msg = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil)
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
|
||||
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
|
||||
userData := userData{
|
||||
baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, errType, errMessage),
|
||||
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage),
|
||||
profileData: l.getProfileData(authReq),
|
||||
}
|
||||
if authReq != nil && authReq.LinkingUsers != nil {
|
||||
@@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI
|
||||
return userData
|
||||
}
|
||||
|
||||
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
|
||||
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
|
||||
title := ""
|
||||
if titleI18nKey != "" {
|
||||
title = translator.LocalizeWithoutArgs(titleI18nKey)
|
||||
@@ -418,7 +416,11 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI
|
||||
}
|
||||
|
||||
func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator {
|
||||
translator, err := l.renderer.NewTranslator(ctx)
|
||||
restrictions, err := l.query.GetInstanceRestrictions(ctx)
|
||||
if err != nil {
|
||||
logging.OnError(err).Warn("cannot load instance restrictions to retrieve allowed languages for creating the translator")
|
||||
}
|
||||
translator, err := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages)
|
||||
logging.OnError(err).Warn("cannot load translator")
|
||||
if authReq != nil {
|
||||
l.addLoginTranslations(translator, authReq.DefaultTranslations)
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/assets"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
)
|
||||
|
||||
type dynamicResourceData struct {
|
||||
@@ -15,8 +16,8 @@ type dynamicResourceData struct {
|
||||
FileName string `schema:"filename"`
|
||||
}
|
||||
|
||||
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler {
|
||||
return http.FileServer(staticDir)
|
||||
func (l *Login) handleResources() http.Handler {
|
||||
return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN))
|
||||
}
|
||||
|
||||
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user