├── .gitignore ├── doc.go ├── Makefile ├── go.mod ├── .github └── workflows │ ├── ci.yml │ └── codeql.yml ├── logic.go ├── docker-compose.yml ├── keycloak_test.go ├── decisionStrategy.go ├── LICENSE.md ├── clientRoles_test.go ├── clientScopes.go ├── realmRoles.go ├── scopes.go ├── groups.go ├── clientScopes_test.go ├── realmRoles_test.go ├── resources.go ├── scopes_test.go ├── resources_test.go ├── realms_test.go ├── clientRoles.go ├── keycloak.go ├── clients_test.go ├── policies_test.go ├── groups_test.go ├── permissions.go ├── policies.go ├── clients.go ├── example_test.go ├── README.md ├── permissions_test.go ├── users.go ├── example_full_test.go ├── users_test.go ├── realms.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | coverage.html 3 | coverage.out 4 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package keycloak provides a client for using the Keycloak API. 2 | package keycloak 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: cov 3 | cov: 4 | go test -v ./... -coverprofile=coverage.out 5 | go tool cover -html=coverage.out -o coverage.html 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zemirco/keycloak/v2 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/go-querystring v1.1.0 7 | golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 8 | ) 9 | 10 | require ( 11 | github.com/golang/protobuf v1.4.2 // indirect 12 | golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect 13 | google.golang.org/appengine v1.6.6 // indirect 14 | google.golang.org/protobuf v1.25.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: ci 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | ci: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: setup go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.16 19 | 20 | - name: run go vet 21 | run: go vet ./... 22 | 23 | - name: test 24 | run: | 25 | docker-compose up -d 26 | sleep 30 27 | go test -race -v ./... 28 | -------------------------------------------------------------------------------- /logic.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | // The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision is obtained. 4 | // 5 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/Logic.java 6 | const ( 7 | // Defines that this policy follows a positive logic. In other words, the final decision is the policy outcome. 8 | LogicPositive = "POSITIVE" 9 | 10 | // Defines that this policy uses a logical negation. In other words, the final decision would be a negative of the policy outcome. 11 | LogicNegative = "NEGATIVE" 12 | ) 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: "3.9" 3 | 4 | volumes: 5 | postgres_data: 6 | driver: local 7 | 8 | services: 9 | 10 | mailhog: 11 | image: mailhog/mailhog:v1.0.1 12 | ports: 13 | - 8025:8025 14 | - 1025:1025 15 | 16 | postgres: 17 | image: postgres:15.0 18 | volumes: 19 | - postgres_data:/var/lib/postgresql/data 20 | environment: 21 | POSTGRES_DB: keycloak 22 | POSTGRES_USER: keycloak 23 | POSTGRES_PASSWORD: password 24 | 25 | keycloak: 26 | image: quay.io/keycloak/keycloak:19.0.3 27 | environment: 28 | DB_VENDOR: postgres 29 | DB_ADDR: postgres 30 | DB_USER: keycloak 31 | DB_PASSWORD: password 32 | KEYCLOAK_ADMIN: admin 33 | KEYCLOAK_ADMIN_PASSWORD: admin 34 | ports: 35 | - 8080:8080 36 | depends_on: 37 | - postgres 38 | command: 39 | - start-dev 40 | -------------------------------------------------------------------------------- /keycloak_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | // create a new keycloak instance. 11 | func client(t *testing.T) *Keycloak { 12 | t.Helper() 13 | 14 | config := oauth2.Config{ 15 | ClientID: "admin-cli", 16 | Endpoint: oauth2.Endpoint{ 17 | TokenURL: "http://localhost:8080/realms/master/protocol/openid-connect/token", 18 | }, 19 | } 20 | 21 | ctx := context.Background() 22 | 23 | token, err := config.PasswordCredentialsToken(ctx, "admin", "admin") 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | 28 | client := config.Client(ctx, token) 29 | 30 | kc, err := NewKeycloak(client, "http://localhost:8080/") 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | return kc 35 | } 36 | 37 | func TestKeycloak_New(t *testing.T) { 38 | 39 | } 40 | 41 | func TestKeycloak_NewRequest(t *testing.T) { 42 | 43 | } 44 | 45 | func TestKeycloak_Do(t *testing.T) { 46 | 47 | } 48 | -------------------------------------------------------------------------------- /decisionStrategy.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | // The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision is obtained. 4 | // 5 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/DecisionStrategy.java 6 | const ( 7 | // AFFIRMATIVE defines that at least one policy must evaluate to a positive decision 8 | // in order to the overall decision be also positive. 9 | DecisionStrategyAffirmative = "AFFIRMATIVE" 10 | 11 | // UNANIMOUS defines that all policies must evaluate to a positive decision 12 | // in order to the overall decision be also positive. 13 | DecisionStrategyUnanimous = "UNANIMOUS" 14 | 15 | // CONSENSUS defines that the number of positive decisions must be greater than the number of negative decisions. 16 | // If the number of positive and negative is the same, the final decision will be negative. 17 | DecisionStrategyConsensus = "CONSENSUS" 18 | ) 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mirco Zeiss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /clientRoles_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // create a new client role 10 | func createClientRole(t *testing.T, k *Keycloak, realm, clientID, roleName string) { 11 | t.Helper() 12 | 13 | role := &Role{ 14 | Name: String(roleName), 15 | Description: String(roleName + " description"), 16 | } 17 | 18 | if _, err := k.ClientRoles.Create(context.Background(), realm, clientID, role); err != nil { 19 | t.Errorf("ClientRoles.Create returned error: %v", err) 20 | } 21 | } 22 | 23 | func TestClientRolesService_Create(t *testing.T) { 24 | k := client(t) 25 | 26 | realm := "first" 27 | createRealm(t, k, realm) 28 | 29 | clientID := createClient(t, k, realm, "client") 30 | 31 | ctx := context.Background() 32 | 33 | role := &Role{ 34 | Name: String("role"), 35 | Description: String("description"), 36 | } 37 | 38 | res, err := k.ClientRoles.Create(ctx, realm, clientID, role) 39 | if err != nil { 40 | t.Errorf("ClientRoles.Create returned error: %v", err) 41 | } 42 | 43 | if res.StatusCode != http.StatusCreated { 44 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 45 | } 46 | } 47 | 48 | func TestClientRolesService_List(t *testing.T) { 49 | k := client(t) 50 | 51 | realm := "first" 52 | createRealm(t, k, realm) 53 | 54 | clientID := createClient(t, k, realm, "client") 55 | 56 | createClientRole(t, k, realm, clientID, "first") 57 | createClientRole(t, k, realm, clientID, "second") 58 | 59 | roles, res, err := k.ClientRoles.List(context.Background(), realm, clientID) 60 | if err != nil { 61 | t.Errorf("ClientRoles.List returned error: %v", err) 62 | } 63 | 64 | if res.StatusCode != http.StatusOK { 65 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 66 | } 67 | 68 | // includes "uma_protection" 69 | if len(roles) != 3 { 70 | t.Errorf("got: %d, want: %d", len(roles), 3) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /clientScopes.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ClientScope representation. 10 | // 11 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/ClientScopeRepresentation.java 12 | type ClientScope struct { 13 | ID *string `json:"id,omitempty"` 14 | Name *string `json:"name,omitempty"` 15 | Description *string `json:"description,omitempty"` 16 | Protocol *string `json:"protocol,omitempty"` 17 | Attributes *map[string]string `json:"attributes,omitempty"` 18 | } 19 | 20 | // ClientScopesService ... 21 | type ClientScopesService service 22 | 23 | // List all client scopes in realm. 24 | func (s *ClientScopesService) List(ctx context.Context, realm string) ([]*ClientScope, *http.Response, error) { 25 | u := fmt.Sprintf("admin/realms/%s/client-scopes", realm) 26 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 27 | if err != nil { 28 | return nil, nil, err 29 | } 30 | 31 | var clientScopes []*ClientScope 32 | res, err := s.keycloak.Do(ctx, req, &clientScopes) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | return clientScopes, res, nil 38 | } 39 | 40 | // Create a new client scope. 41 | func (s *ClientScopesService) Create(ctx context.Context, realm string, clientScope *ClientScope) (*http.Response, error) { 42 | u := fmt.Sprintf("admin/realms/%s/client-scopes", realm) 43 | req, err := s.keycloak.NewRequest(http.MethodPost, u, clientScope) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return s.keycloak.Do(ctx, req, nil) 49 | } 50 | 51 | // Get client scope. 52 | func (s *ClientScopesService) Get(ctx context.Context, realm, clientScopeID string) (*ClientScope, *http.Response, error) { 53 | u := fmt.Sprintf("admin/realms/%s/client-scopes/%s", realm, clientScopeID) 54 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | var clientScope ClientScope 60 | res, err := s.keycloak.Do(ctx, req, &clientScope) 61 | if err != nil { 62 | return nil, nil, err 63 | } 64 | 65 | return &clientScope, res, nil 66 | } 67 | 68 | // Delete client scope. 69 | func (s *ClientScopesService) Delete(ctx context.Context, realm, clientScopeID string) (*http.Response, error) { 70 | u := fmt.Sprintf("admin/realms/%s/client-scopes/%s", realm, clientScopeID) 71 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return s.keycloak.Do(ctx, req, nil) 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "codeql" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '39 6 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /realmRoles.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Role representation 10 | type Role struct { 11 | ID *string `json:"id,omitempty"` 12 | Name *string `json:"name,omitempty"` 13 | Description *string `json:"description,omitempty"` 14 | Composite *bool `json:"composite,omitempty"` 15 | ClientRole *bool `json:"clientRole,omitempty"` 16 | ContainerID *string `json:"containerId,omitempty"` 17 | Attributes *map[string][]string `json:"attributes,omitempty"` 18 | } 19 | 20 | // RealmRolesService ... 21 | type RealmRolesService service 22 | 23 | // Create a new role. 24 | func (s *RealmRolesService) Create(ctx context.Context, realm string, role *Role) (*http.Response, error) { 25 | u := fmt.Sprintf("admin/realms/%s/roles", realm) 26 | req, err := s.keycloak.NewRequest(http.MethodPost, u, role) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return s.keycloak.Do(ctx, req, nil) 32 | } 33 | 34 | // RolesListOptions ... 35 | type RolesListOptions struct { 36 | BriefRepresentation bool `url:"briefRepresentation,omitempty"` 37 | Search bool `url:"search,omitempty"` 38 | Options 39 | } 40 | 41 | // List roles. 42 | func (s *RealmRolesService) List(ctx context.Context, realm string, opts *RolesListOptions) ([]*Role, *http.Response, error) { 43 | u := fmt.Sprintf("admin/realms/%s/roles", realm) 44 | u, err := addOptions(u, opts) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | var roles []*Role 55 | res, err := s.keycloak.Do(ctx, req, &roles) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | return roles, res, nil 61 | } 62 | 63 | // GetByName gets role by name. 64 | func (s *RealmRolesService) GetByName(ctx context.Context, realm, name string) (*Role, *http.Response, error) { 65 | u := fmt.Sprintf("admin/realms/%s/roles/%s", realm, name) 66 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 67 | if err != nil { 68 | return nil, nil, err 69 | } 70 | 71 | var role Role 72 | res, err := s.keycloak.Do(ctx, req, &role) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | return &role, res, nil 78 | } 79 | 80 | // GetByID gets role by id. 81 | func (s *RealmRolesService) GetByID(ctx context.Context, realm, roleID string) (*Role, *http.Response, error) { 82 | u := fmt.Sprintf("admin/realms/%s/roles-by-id/%s", realm, roleID) 83 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | 88 | var role Role 89 | res, err := s.keycloak.Do(ctx, req, &role) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | 94 | return &role, res, nil 95 | } 96 | 97 | // delete role 98 | 99 | // delete client role 100 | -------------------------------------------------------------------------------- /scopes.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ScopesService handles communication with the scopes related methods of the Keycloak API. 10 | type ScopesService service 11 | 12 | // Scope represents a Keycloak scope. 13 | // 14 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java 15 | type Scope struct { 16 | ID *string `json:"id,omitempty"` 17 | Name *string `json:"name,omitempty"` 18 | IconURI *string `json:"iconUri,omitempty"` 19 | DisplayName *string `json:"displayName,omitempty"` 20 | } 21 | 22 | // List lists all resources. 23 | func (s *ScopesService) List(ctx context.Context, realm, clientID string) ([]*Scope, *http.Response, error) { 24 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/scope", realm, clientID) 25 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 26 | if err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | var scopes []*Scope 31 | res, err := s.keycloak.Do(ctx, req, &scopes) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | 36 | return scopes, res, nil 37 | } 38 | 39 | // Create creates a new scope. 40 | func (s *ScopesService) Create(ctx context.Context, realm, clientID string, scope *Scope) (*Scope, *http.Response, error) { 41 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/scope", realm, clientID) 42 | req, err := s.keycloak.NewRequest(http.MethodPost, u, scope) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | var created Scope 48 | res, err := s.keycloak.Do(ctx, req, &created) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | return &created, res, nil 54 | } 55 | 56 | // Get gets a single scope. 57 | func (s *ScopesService) Get(ctx context.Context, realm, clientID, scopeID string) (*Scope, *http.Response, error) { 58 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/scope/%s", realm, clientID, scopeID) 59 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | 64 | var scope Scope 65 | res, err := s.keycloak.Do(ctx, req, &scope) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | return &scope, res, nil 71 | } 72 | 73 | // Delete deletes a single scope. 74 | func (s *ScopesService) Delete(ctx context.Context, realm, clientID, scopeID string) (*http.Response, error) { 75 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/scope/%s", realm, clientID, scopeID) 76 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return s.keycloak.Do(ctx, req, nil) 82 | } 83 | 84 | // Update creates a new scope. 85 | func (s *ScopesService) Update(ctx context.Context, realm, clientID string, scope *Scope) (*http.Response, error) { 86 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/scope/%s", realm, clientID, *scope.ID) 87 | req, err := s.keycloak.NewRequest(http.MethodPut, u, scope) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return s.keycloak.Do(ctx, req, nil) 93 | } 94 | -------------------------------------------------------------------------------- /groups.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Group ... 10 | type Group struct { 11 | ID *string `json:"id,omitempty"` 12 | Name *string `json:"name,omitempty"` 13 | Path *string `json:"path,omitempty"` 14 | Attributes *map[string][]string `json:"attributes,omitempty"` 15 | RealmRoles []string `json:"realmRoles,omitempty"` 16 | ClientRoles *map[string][]string `json:"clientRoles,omitempty"` 17 | SubGroups []*Group `json:"subGroups,omitempty"` 18 | Access *map[string]bool `json:"access,omitempty"` 19 | } 20 | 21 | // GroupsService ... 22 | type GroupsService service 23 | 24 | // Create a new group. 25 | func (s *GroupsService) Create(ctx context.Context, realm string, group *Group) (*http.Response, error) { 26 | u := fmt.Sprintf("admin/realms/%s/groups", realm) 27 | req, err := s.keycloak.NewRequest(http.MethodPost, u, group) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return s.keycloak.Do(ctx, req, nil) 33 | } 34 | 35 | // List groups. 36 | func (s *GroupsService) List(ctx context.Context, realm string) ([]*Group, *http.Response, error) { 37 | u := fmt.Sprintf("admin/realms/%s/groups", realm) 38 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | 43 | var groups []*Group 44 | res, err := s.keycloak.Do(ctx, req, &groups) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | return groups, res, nil 50 | } 51 | 52 | // Get group. 53 | func (s *GroupsService) Get(ctx context.Context, realm, groupID string) (*Group, *http.Response, error) { 54 | u := fmt.Sprintf("admin/realms/%s/groups/%s", realm, groupID) 55 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | var group Group 61 | res, err := s.keycloak.Do(ctx, req, &group) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | return &group, res, nil 67 | } 68 | 69 | // update group 70 | 71 | // Delete group. 72 | func (s *GroupsService) Delete(ctx context.Context, realm, groupID string) (*http.Response, error) { 73 | u := fmt.Sprintf("admin/realms/%s/groups/%s", realm, groupID) 74 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return s.keycloak.Do(ctx, req, nil) 80 | } 81 | 82 | func (s *GroupsService) AddRealmRoles(ctx context.Context, realm, groupID string, roles []*Role) (*http.Response, error) { 83 | u := fmt.Sprintf("admin/realms/%s/groups/%s/role-mappings/realm", realm, groupID) 84 | req, err := s.keycloak.NewRequest(http.MethodPost, u, roles) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return s.keycloak.Do(ctx, req, nil) 90 | } 91 | 92 | func (s *GroupsService) RemoveRealmRoles(ctx context.Context, realm, groupID string, roles []*Role) (*http.Response, error) { 93 | u := fmt.Sprintf("admin/realms/%s/groups/%s/role-mappings/realm", realm, groupID) 94 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, roles) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return s.keycloak.Do(ctx, req, nil) 100 | } 101 | -------------------------------------------------------------------------------- /clientScopes_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // create a new client scope. 11 | func createClientScope(t *testing.T, k *Keycloak, realm string, clientScopeName string) string { 12 | t.Helper() 13 | 14 | clientScope := &ClientScope{ 15 | Name: String(clientScopeName), 16 | Description: String(clientScopeName + " description"), 17 | } 18 | 19 | res, err := k.ClientScopes.Create(context.Background(), realm, clientScope) 20 | if err != nil { 21 | t.Errorf("ClientScopes.Create returned error: %v", err) 22 | } 23 | 24 | parts := strings.Split(res.Header.Get("Location"), "/") 25 | clientScopeID := parts[len(parts)-1] 26 | return clientScopeID 27 | } 28 | 29 | func TestClientScopesService_List(t *testing.T) { 30 | k := client(t) 31 | 32 | realm := "first" 33 | createRealm(t, k, realm) 34 | 35 | clientScopes, res, err := k.ClientScopes.List(context.Background(), realm) 36 | if err != nil { 37 | t.Errorf("ClientScopes.List returned error: %v", err) 38 | } 39 | 40 | if res.StatusCode != http.StatusOK { 41 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 42 | } 43 | 44 | // ...-dedicated, acr, address, email, microprofile-jwt, offline_access, phone, profile, roles, web-origins 45 | if len(clientScopes) != 10 { 46 | t.Errorf("got: %d, want: %d", len(clientScopes), 10) 47 | } 48 | } 49 | 50 | func TestClientScopesService_Create(t *testing.T) { 51 | k := client(t) 52 | 53 | realm := "first" 54 | createRealm(t, k, realm) 55 | 56 | ctx := context.Background() 57 | 58 | clientScope := &ClientScope{ 59 | Name: String("my-client-scope"), 60 | Description: String("some description"), 61 | } 62 | 63 | res, err := k.ClientScopes.Create(ctx, realm, clientScope) 64 | if err != nil { 65 | t.Errorf("ClientScopes.Create returned error: %v", err) 66 | } 67 | 68 | if res.StatusCode != http.StatusCreated { 69 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 70 | } 71 | } 72 | 73 | func TestClientScopesService_Get(t *testing.T) { 74 | k := client(t) 75 | 76 | realm := "first" 77 | createRealm(t, k, realm) 78 | 79 | clientScopeID := createClientScope(t, k, realm, "my-client-scope") 80 | 81 | clientScopes, res, err := k.ClientScopes.Get(context.Background(), realm, clientScopeID) 82 | if err != nil { 83 | t.Errorf("ClientScopes.Get returned error: %v", err) 84 | } 85 | 86 | if res.StatusCode != http.StatusOK { 87 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 88 | } 89 | 90 | if *clientScopes.Name != "my-client-scope" { 91 | t.Errorf("got: %s, want: %s", *clientScopes.Name, "my-client-scope") 92 | } 93 | } 94 | 95 | func TestClientScopesService_Delete(t *testing.T) { 96 | k := client(t) 97 | 98 | realm := "first" 99 | createRealm(t, k, realm) 100 | 101 | clientScopeID := createClientScope(t, k, realm, "my-client-scope") 102 | 103 | res, err := k.ClientScopes.Delete(context.Background(), realm, clientScopeID) 104 | if err != nil { 105 | t.Errorf("ClientScopes.Delete returned error: %v", err) 106 | } 107 | 108 | if res.StatusCode != http.StatusNoContent { 109 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /realmRoles_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // create a new realm role. 10 | func createRealmRole(t *testing.T, k *Keycloak, realm string, name string) { 11 | t.Helper() 12 | 13 | role := &Role{ 14 | Name: String(name), 15 | Description: String(name + " description"), 16 | } 17 | 18 | if _, err := k.RealmRoles.Create(context.Background(), realm, role); err != nil { 19 | t.Errorf("RealmRoles.Create returned error: %v", err) 20 | } 21 | } 22 | 23 | func TestRealmRolesService_Create(t *testing.T) { 24 | k := client(t) 25 | 26 | realm := "first" 27 | createRealm(t, k, realm) 28 | 29 | ctx := context.Background() 30 | 31 | role := &Role{ 32 | Name: String("my name"), 33 | Description: String("my description"), 34 | } 35 | 36 | res, err := k.RealmRoles.Create(ctx, realm, role) 37 | if err != nil { 38 | t.Errorf("RealmRoles.Create returned error: %v", err) 39 | } 40 | 41 | if res.StatusCode != http.StatusCreated { 42 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 43 | } 44 | } 45 | 46 | func TestRealmRolesService_List(t *testing.T) { 47 | k := client(t) 48 | 49 | realm := "first" 50 | createRealm(t, k, realm) 51 | 52 | createRealmRole(t, k, realm, "first") 53 | createRealmRole(t, k, realm, "second") 54 | 55 | roles, res, err := k.RealmRoles.List(context.Background(), realm, nil) 56 | if err != nil { 57 | t.Errorf("RealmRoles.List returned error: %v", err) 58 | } 59 | 60 | if res.StatusCode != http.StatusOK { 61 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 62 | } 63 | 64 | // it includes the "offline_access", "uma_authorization" and "default-roles-..." roles 65 | if len(roles) != 5 { 66 | t.Errorf("got: %d, want: %d", len(roles), 5) 67 | } 68 | } 69 | 70 | func TestRealmRolesService_GetByName(t *testing.T) { 71 | k := client(t) 72 | 73 | realm := "first" 74 | createRealm(t, k, realm) 75 | 76 | createRealmRole(t, k, realm, "first") 77 | 78 | role, res, err := k.RealmRoles.GetByName(context.Background(), realm, "first") 79 | if err != nil { 80 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 81 | } 82 | 83 | if res.StatusCode != http.StatusOK { 84 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 85 | } 86 | 87 | if *role.Name != "first" { 88 | t.Errorf("got: %s, want: %s", *role.Name, "first") 89 | } 90 | } 91 | 92 | func TestRealmRolesService_GetByID(t *testing.T) { 93 | k := client(t) 94 | 95 | realm := "first" 96 | createRealm(t, k, realm) 97 | 98 | createRealmRole(t, k, realm, "first") 99 | 100 | // get by name first to get id 101 | role, _, err := k.RealmRoles.GetByName(context.Background(), realm, "first") 102 | if err != nil { 103 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 104 | } 105 | 106 | // now get by id 107 | role, res, err := k.RealmRoles.GetByID(context.Background(), realm, *role.ID) 108 | if err != nil { 109 | t.Errorf("RealmRoles.GetByID returned error: %v", err) 110 | } 111 | 112 | if res.StatusCode != http.StatusOK { 113 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 114 | } 115 | 116 | if *role.Name != "first" { 117 | t.Errorf("got: %s, want: %s", *role.Name, "first") 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ResourcesService handles communication with the resources related methods of the Keycloak API. 10 | type ResourcesService service 11 | 12 | // ResourceOwner represents a Keycloak resource owner. 13 | // 14 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java 15 | type ResourceOwner struct { 16 | ID *string `json:"id,omitempty"` 17 | Name *string `json:"name,omitempty"` 18 | } 19 | 20 | // Resource represents a Keycloak resource. 21 | // 22 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 23 | type Resource struct { 24 | ID *string `json:"_id,omitempty"` 25 | Scopes []*Scope `json:"scopes,omitempty"` 26 | Attributes *map[string][]string `json:"attributes,omitempty"` 27 | Uris []string `json:"uris,omitempty"` 28 | Name *string `json:"name,omitempty"` 29 | OwnerManagedAccess *bool `json:"ownerManagedAccess,omitempty"` 30 | DisplayName *string `json:"displayName,omitempty"` 31 | Owner *ResourceOwner `json:"owner,omitempty"` 32 | } 33 | 34 | // List lists all resources. 35 | func (s *ResourcesService) List(ctx context.Context, realm, clientID string) ([]*Resource, *http.Response, error) { 36 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/resource", realm, clientID) 37 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | 42 | var resources []*Resource 43 | res, err := s.keycloak.Do(ctx, req, &resources) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | 48 | return resources, res, nil 49 | } 50 | 51 | // Create creates a new resource. 52 | func (s *ResourcesService) Create(ctx context.Context, realm, clientID string, resource *Resource) (*Resource, *http.Response, error) { 53 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/resource", realm, clientID) 54 | req, err := s.keycloak.NewRequest(http.MethodPost, u, resource) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | var created Resource 60 | res, err := s.keycloak.Do(ctx, req, &created) 61 | if err != nil { 62 | return nil, nil, err 63 | } 64 | 65 | return &created, res, nil 66 | } 67 | 68 | // Get gets a single resource. 69 | func (s *ResourcesService) Get(ctx context.Context, realm, clientID, resourceID string) (*Resource, *http.Response, error) { 70 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/resource/%s", realm, clientID, resourceID) 71 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | var resource Resource 77 | res, err := s.keycloak.Do(ctx, req, &resource) 78 | if err != nil { 79 | return nil, nil, err 80 | } 81 | 82 | return &resource, res, nil 83 | } 84 | 85 | // Delete deletes a single resource. 86 | func (s *ResourcesService) Delete(ctx context.Context, realm, clientID, resourceID string) (*http.Response, error) { 87 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/resource/%s", realm, clientID, resourceID) 88 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return s.keycloak.Do(ctx, req, nil) 94 | } 95 | -------------------------------------------------------------------------------- /scopes_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func createScope(t *testing.T, k *Keycloak, realm, clientID, name string) *Scope { 10 | t.Helper() 11 | 12 | scope := &Scope{ 13 | Name: String(name), 14 | } 15 | 16 | scope, _, err := k.Scopes.Create(context.Background(), realm, clientID, scope) 17 | if err != nil { 18 | t.Errorf("Scopes.Create returned error: %v", err) 19 | } 20 | return scope 21 | } 22 | 23 | func TestScopesService_Create(t *testing.T) { 24 | k := client(t) 25 | 26 | realm := "first" 27 | createRealm(t, k, realm) 28 | clientID := createClient(t, k, realm, "client") 29 | 30 | scope := &Scope{ 31 | Name: String("scope_read"), 32 | } 33 | 34 | scope, res, err := k.Scopes.Create(context.Background(), realm, clientID, scope) 35 | if err != nil { 36 | t.Errorf("Scopes.Create returned error: %v", err) 37 | } 38 | 39 | if res.StatusCode != http.StatusCreated { 40 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 41 | } 42 | 43 | if *scope.Name != "scope_read" { 44 | t.Errorf("got: %s, want: %s", *scope.Name, "scope_read") 45 | } 46 | } 47 | 48 | func TestScopesService_List(t *testing.T) { 49 | k := client(t) 50 | realm := "first" 51 | createRealm(t, k, realm) 52 | clientID := createClient(t, k, realm, "client") 53 | 54 | createScope(t, k, realm, clientID, "read") 55 | createScope(t, k, realm, clientID, "write") 56 | 57 | scopes, res, err := k.Scopes.List(context.Background(), realm, clientID) 58 | if err != nil { 59 | t.Errorf("Scopes.List returned error: %v", err) 60 | } 61 | 62 | if res.StatusCode != http.StatusOK { 63 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 64 | } 65 | 66 | if len(scopes) != 2 { 67 | t.Errorf("got: %d, want: %d", len(scopes), 2) 68 | } 69 | } 70 | 71 | func TestScopesService_Get(t *testing.T) { 72 | k := client(t) 73 | realm := "first" 74 | createRealm(t, k, realm) 75 | clientID := createClient(t, k, realm, "client") 76 | 77 | scope := createScope(t, k, realm, clientID, "read") 78 | 79 | scope, res, err := k.Scopes.Get(context.Background(), realm, clientID, *scope.ID) 80 | if err != nil { 81 | t.Errorf("Scopes.Get returned error: %v", err) 82 | } 83 | 84 | if res.StatusCode != http.StatusOK { 85 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 86 | } 87 | 88 | if *scope.Name != "read" { 89 | t.Errorf("got: %s, want: %s", *scope.Name, "read") 90 | } 91 | } 92 | 93 | func TestScopesService_Delete(t *testing.T) { 94 | k := client(t) 95 | realm := "first" 96 | createRealm(t, k, realm) 97 | clientID := createClient(t, k, realm, "client") 98 | 99 | scope := createScope(t, k, realm, clientID, "read") 100 | 101 | res, err := k.Scopes.Delete(context.Background(), realm, clientID, *scope.ID) 102 | if err != nil { 103 | t.Errorf("Scopes.Delete returned error: %v", err) 104 | } 105 | 106 | if res.StatusCode != http.StatusNoContent { 107 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 108 | } 109 | } 110 | 111 | func TestScopesService_Update(t *testing.T) { 112 | k := client(t) 113 | realm := "first" 114 | createRealm(t, k, realm) 115 | clientID := createClient(t, k, realm, "client") 116 | 117 | scope := createScope(t, k, realm, clientID, "read") 118 | 119 | scope.Name = String("updated") 120 | 121 | res, err := k.Scopes.Update(context.Background(), realm, clientID, scope) 122 | if err != nil { 123 | t.Errorf("Scopes.Update returned error: %v", err) 124 | } 125 | 126 | if res.StatusCode != http.StatusNoContent { 127 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /resources_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // create a new resource. 10 | func createResource(t *testing.T, k *Keycloak, realm, clientID, name string) *Resource { 11 | t.Helper() 12 | 13 | resource := &Resource{ 14 | Name: String(name), 15 | DisplayName: String(name), 16 | } 17 | 18 | created, _, err := k.Resources.Create(context.Background(), realm, clientID, resource) 19 | if err != nil { 20 | t.Errorf("Resources.Create returned error: %v", err) 21 | } 22 | 23 | return created 24 | } 25 | 26 | func TestResourcesService_Create(t *testing.T) { 27 | k := client(t) 28 | 29 | realm := "first" 30 | createRealm(t, k, realm) 31 | 32 | clientID := createClient(t, k, realm, "client") 33 | 34 | ctx := context.Background() 35 | 36 | resource := &Resource{ 37 | Name: String("resource"), 38 | DisplayName: String("resource"), 39 | } 40 | 41 | created, res, err := k.Resources.Create(ctx, realm, clientID, resource) 42 | if err != nil { 43 | t.Errorf("Clients.CreateResource returned error: %v", err) 44 | } 45 | 46 | if res.StatusCode != http.StatusCreated { 47 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 48 | } 49 | 50 | if *created.Name != "resource" { 51 | t.Errorf("got: %s, want: %s", *resource.Name, "Default Resource") 52 | } 53 | } 54 | 55 | func TestResourcesService_List(t *testing.T) { 56 | k := client(t) 57 | 58 | realm := "first" 59 | createRealm(t, k, realm) 60 | 61 | clientID := createClient(t, k, realm, "client") 62 | 63 | resources, res, err := k.Resources.List(context.Background(), realm, clientID) 64 | if err != nil { 65 | t.Errorf("Clients.ListResources returned error: %v", err) 66 | } 67 | 68 | if res.StatusCode != http.StatusOK { 69 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 70 | } 71 | 72 | if len(resources) != 1 { 73 | t.Errorf("got: %d, want: %d", len(resources), 1) 74 | } 75 | } 76 | 77 | func TestResourcesService_Get(t *testing.T) { 78 | k := client(t) 79 | 80 | realm := "first" 81 | createRealm(t, k, realm) 82 | 83 | clientID := createClient(t, k, realm, "client") 84 | 85 | // list all resources first 86 | resources, _, err := k.Resources.List(context.Background(), realm, clientID) 87 | if err != nil { 88 | t.Errorf("Clients.ListResources returned error: %v", err) 89 | } 90 | 91 | resource, res, err := k.Resources.Get(context.Background(), realm, clientID, *resources[0].ID) 92 | if err != nil { 93 | t.Errorf("Clients.GetResource returned error: %v", err) 94 | } 95 | 96 | if res.StatusCode != http.StatusOK { 97 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 98 | } 99 | 100 | if *resource.Name != "Default Resource" { 101 | t.Errorf("got: %s, want: %s", *resource.Name, "Default Resource") 102 | } 103 | } 104 | 105 | func TestResourcesService_Deletee(t *testing.T) { 106 | k := client(t) 107 | 108 | realm := "first" 109 | createRealm(t, k, realm) 110 | 111 | clientID := createClient(t, k, realm, "client") 112 | 113 | ctx := context.Background() 114 | 115 | resource := &Resource{ 116 | Name: String("resource"), 117 | DisplayName: String("resource"), 118 | } 119 | 120 | created, _, err := k.Resources.Create(ctx, realm, clientID, resource) 121 | if err != nil { 122 | t.Errorf("Clients.CreateResource returned error: %v", err) 123 | } 124 | 125 | res, err := k.Resources.Delete(ctx, realm, clientID, *created.ID) 126 | if err != nil { 127 | t.Errorf("Clients.DeleteResource returned error: %v", err) 128 | } 129 | 130 | if res.StatusCode != http.StatusNoContent { 131 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /realms_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // create a new realm and delete it afterwards. 10 | func createRealm(t *testing.T, k *Keycloak, name string) { 11 | t.Helper() 12 | 13 | realm := &Realm{ 14 | Enabled: Bool(true), 15 | ID: String(name), 16 | Realm: String(name), 17 | SMTPServer: &map[string]string{ 18 | "from": "john@wayne.com", 19 | "host": "mailhog", 20 | "port": "1025", 21 | }, 22 | } 23 | 24 | ctx := context.Background() 25 | 26 | // create a new realm 27 | if _, err := k.Realms.Create(ctx, realm); err != nil { 28 | t.Errorf("Realms.Create returned error: %v", err) 29 | } 30 | 31 | t.Cleanup(func() { 32 | if _, err := k.Realms.Delete(ctx, name); err != nil { 33 | t.Errorf("Realms.Delete returned error: %v", err) 34 | } 35 | }) 36 | } 37 | 38 | func TestRealmsService_Create(t *testing.T) { 39 | k := client(t) 40 | 41 | name := "supernice" 42 | ctx := context.Background() 43 | 44 | realm := &Realm{ 45 | Enabled: Bool(true), 46 | ID: String(name), 47 | Realm: String(name), 48 | } 49 | 50 | res, err := k.Realms.Create(ctx, realm) 51 | if err != nil { 52 | t.Errorf("Realms.Create returned error: %v", err) 53 | } 54 | 55 | if res.StatusCode != http.StatusCreated { 56 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 57 | } 58 | 59 | if res.Header.Get("Location") != "http://localhost:8080/admin/realms/supernice" { 60 | t.Errorf("got: %s, want: %s", res.Header.Get("Location"), "http://localhost:8080/admin/realms/supernice") 61 | } 62 | 63 | // manually clean up 64 | if _, err := k.Realms.Delete(ctx, name); err != nil { 65 | t.Errorf("Realms.Delete returned error: %v", err) 66 | } 67 | } 68 | 69 | func TestRealmsService_List(t *testing.T) { 70 | k := client(t) 71 | 72 | createRealm(t, k, "first") 73 | createRealm(t, k, "second") 74 | 75 | realms, res, err := k.Realms.List(context.Background()) 76 | if err != nil { 77 | t.Errorf("Realms.List returned error: %v", err) 78 | } 79 | 80 | if res.StatusCode != http.StatusOK { 81 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 82 | } 83 | 84 | // it includes the master realm 85 | if len(realms) != 3 { 86 | t.Errorf("got: %d, want: %d", len(realms), 3) 87 | } 88 | } 89 | 90 | func TestRealmsService_Get(t *testing.T) { 91 | k := client(t) 92 | 93 | createRealm(t, k, "first") 94 | 95 | realm, res, err := k.Realms.Get(context.Background(), "first") 96 | if err != nil { 97 | t.Errorf("Realms.Get returned error: %v", err) 98 | } 99 | 100 | if res.StatusCode != http.StatusOK { 101 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 102 | } 103 | 104 | if *realm.ID != "first" { 105 | t.Errorf("got: %s, want: %s", *realm.ID, "first") 106 | } 107 | } 108 | 109 | func TestRealmsService_Delete(t *testing.T) { 110 | k := client(t) 111 | 112 | createRealm(t, k, "first") 113 | 114 | res, err := k.Realms.Delete(context.Background(), "first") 115 | if err != nil { 116 | t.Errorf("Realms.Delete returned error: %v", err) 117 | } 118 | 119 | if res.StatusCode != http.StatusNoContent { 120 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 121 | } 122 | } 123 | 124 | func TestRealmsService_GetConfig(t *testing.T) { 125 | k := client(t) 126 | 127 | createRealm(t, k, "first") 128 | 129 | config, res, err := k.Realms.GetConfig(context.Background(), "first") 130 | if err != nil { 131 | t.Errorf("Realms.GetConfig returned error: %v", err) 132 | } 133 | 134 | if res.StatusCode != http.StatusOK { 135 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 136 | } 137 | 138 | if *config.Issuer != "http://localhost:8080/realms/first" { 139 | t.Errorf("got: %s, want: %s", *config.Issuer, "http://localhost:8080/realms/first") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /clientRoles.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ClientRolesService handles communication with the client roles related methods of the Keycloak API. 10 | type ClientRolesService service 11 | 12 | // Create creates a new client role. 13 | func (s *ClientRolesService) Create(ctx context.Context, realm, id string, role *Role) (*http.Response, error) { 14 | u := fmt.Sprintf("admin/realms/%s/clients/%s/roles", realm, id) 15 | req, err := s.keycloak.NewRequest(http.MethodPost, u, role) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return s.keycloak.Do(ctx, req, nil) 21 | } 22 | 23 | // List lists all client roles. 24 | func (s *ClientRolesService) List(ctx context.Context, realm, id string) ([]*Role, *http.Response, error) { 25 | u := fmt.Sprintf("admin/realms/%s/clients/%s/roles", realm, id) 26 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 27 | if err != nil { 28 | return nil, nil, err 29 | } 30 | 31 | var roles []*Role 32 | res, err := s.keycloak.Do(ctx, req, &roles) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | return roles, res, nil 38 | } 39 | 40 | // Get retrieves a single client role. 41 | func (s *ClientRolesService) Get(ctx context.Context, realm, id, roleName string) (*Role, *http.Response, error) { 42 | u := fmt.Sprintf("admin/realms/%s/clients/%s/roles/%s", realm, id, roleName) 43 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | 48 | var role *Role 49 | res, err := s.keycloak.Do(ctx, req, &role) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | return role, res, nil 55 | } 56 | 57 | // GetUsers returns a stream of users that have the specified role name. 58 | func (s *ClientRolesService) GetUsers(ctx context.Context, realm, clientID, role string, opts *Options) ([]*User, *http.Response, error) { 59 | u := fmt.Sprintf("admin/realms/%s/clients/%s/roles/%s/users", realm, clientID, role) 60 | u, err := addOptions(u, opts) 61 | if err != nil { 62 | return nil, nil, err 63 | } 64 | 65 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | var users []*User 71 | res, err := s.keycloak.Do(ctx, req, &users) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | return users, res, nil 77 | } 78 | 79 | // GetByID gets client role by id. 80 | func (s *ClientRolesService) GetByID(ctx context.Context, realm, roleID, clientID string) (*Role, *http.Response, error) { 81 | u := fmt.Sprintf("admin/realms/%s/roles-by-id/%s?client=%s", realm, roleID, clientID) 82 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | var role Role 88 | res, err := s.keycloak.Do(ctx, req, &role) 89 | if err != nil { 90 | return nil, nil, err 91 | } 92 | 93 | return &role, res, nil 94 | } 95 | 96 | // DeleteByID deletes client role by id. 97 | func (s *ClientRolesService) DeleteByID(ctx context.Context, realm, roleID, clientID string) (*http.Response, error) { 98 | u := fmt.Sprintf("admin/realms/%s/roles-by-id/%s?client=%s", realm, roleID, clientID) 99 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return s.keycloak.Do(ctx, req, nil) 104 | } 105 | 106 | // Returns a stream of groups that have the specified role name 107 | // GET /{realm}/clients/{id}/roles/{role-name}/groups 108 | 109 | // Add a composite to the role 110 | // POST /{realm}/clients/{id}/roles/{role-name}/composites 111 | 112 | // Get composites of the role 113 | // GET /{realm}/clients/{id}/roles/{role-name}/composites 114 | 115 | // Remove roles from the role’s composite 116 | // DELETE /{realm}/clients/{id}/roles/{role-name}/composites 117 | -------------------------------------------------------------------------------- /keycloak.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/google/go-querystring/query" 15 | ) 16 | 17 | // Keycloak ... 18 | type Keycloak struct { 19 | client *http.Client 20 | 21 | BaseURL *url.URL 22 | 23 | common service 24 | 25 | Clients *ClientsService 26 | ClientRoles *ClientRolesService 27 | ClientScopes *ClientScopesService 28 | Groups *GroupsService 29 | Permissions *PermissionsService 30 | Policies *PoliciesService 31 | Realms *RealmsService 32 | RealmRoles *RealmRolesService 33 | Resources *ResourcesService 34 | Scopes *ScopesService 35 | Users *UsersService 36 | } 37 | 38 | type service struct { 39 | keycloak *Keycloak 40 | } 41 | 42 | // addOptions adds the parameters in opts as URL query parameters to s. opts 43 | // must be a struct whose fields may contain "url" tags. 44 | func addOptions(s string, opts interface{}) (string, error) { 45 | v := reflect.ValueOf(opts) 46 | if v.Kind() == reflect.Ptr && v.IsNil() { 47 | return s, nil 48 | } 49 | 50 | u, err := url.Parse(s) 51 | if err != nil { 52 | return s, err 53 | } 54 | 55 | qs, err := query.Values(opts) 56 | if err != nil { 57 | return s, err 58 | } 59 | 60 | u.RawQuery = qs.Encode() 61 | return u.String(), nil 62 | } 63 | 64 | // NewKeycloak ... 65 | func NewKeycloak(httpClient *http.Client, baseURL string) (*Keycloak, error) { 66 | if httpClient == nil { 67 | httpClient = &http.Client{} 68 | } 69 | uri, err := url.Parse(baseURL) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | k := &Keycloak{ 75 | client: httpClient, 76 | BaseURL: uri, 77 | } 78 | 79 | k.common.keycloak = k 80 | k.Clients = (*ClientsService)(&k.common) 81 | k.ClientRoles = (*ClientRolesService)(&k.common) 82 | k.ClientScopes = (*ClientScopesService)(&k.common) 83 | k.Groups = (*GroupsService)(&k.common) 84 | k.Permissions = (*PermissionsService)(&k.common) 85 | k.Policies = (*PoliciesService)(&k.common) 86 | k.Realms = (*RealmsService)(&k.common) 87 | k.RealmRoles = (*RealmRolesService)(&k.common) 88 | k.Resources = (*ResourcesService)(&k.common) 89 | k.Scopes = (*ScopesService)(&k.common) 90 | k.Users = (*UsersService)(&k.common) 91 | 92 | return k, nil 93 | } 94 | 95 | // NewRequest ... 96 | func (k *Keycloak) NewRequest(method string, url string, body interface{}) (*http.Request, error) { 97 | if !strings.HasSuffix(k.BaseURL.Path, "/") { 98 | return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", k.BaseURL) 99 | } 100 | u, err := k.BaseURL.Parse(url) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | var b io.ReadWriter 106 | if body != nil { 107 | b = &bytes.Buffer{} 108 | if err := json.NewEncoder(b).Encode(body); err != nil { 109 | return nil, err 110 | } 111 | } 112 | 113 | req, err := http.NewRequest(method, u.String(), b) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | if body != nil { 119 | req.Header.Set("Content-Type", "application/json") 120 | } 121 | 122 | return req, nil 123 | } 124 | 125 | // Do ... 126 | func (k *Keycloak) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 127 | req = req.WithContext(ctx) 128 | 129 | res, err := k.client.Do(req) 130 | if err != nil { 131 | return nil, err 132 | } 133 | defer res.Body.Close() 134 | 135 | if v != nil { 136 | if err := json.NewDecoder(res.Body).Decode(v); err != nil { 137 | return nil, err 138 | } 139 | } 140 | 141 | return res, err 142 | } 143 | 144 | // Bool is a helper routine that allocates a new bool value 145 | // to store v and returns a pointer to it. 146 | func Bool(v bool) *bool { return &v } 147 | 148 | // // Int is a helper routine that allocates a new int value 149 | // // to store v and returns a pointer to it. 150 | // func Int(v int) *int { return &v } 151 | 152 | // // Int64 is a helper routine that allocates a new int64 value 153 | // // to store v and returns a pointer to it. 154 | // func Int64(v int64) *int64 { return &v } 155 | 156 | // String is a helper routine that allocates a new string value 157 | // to store v and returns a pointer to it. 158 | func String(v string) *string { return &v } 159 | -------------------------------------------------------------------------------- /clients_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // create a new client. 11 | func createClient(t *testing.T, k *Keycloak, realm string, clientID string) string { 12 | t.Helper() 13 | 14 | client := &Client{ 15 | Enabled: Bool(true), 16 | ClientID: String(clientID), 17 | RedirectUris: []string{"http://localhost:4200/*"}, 18 | AuthorizationServicesEnabled: Bool(true), 19 | ServiceAccountsEnabled: Bool(true), 20 | PublicClient: Bool(false), 21 | } 22 | 23 | res, err := k.Clients.Create(context.Background(), realm, client) 24 | if err != nil { 25 | t.Errorf("Clients.Create returned error: %v", err) 26 | } 27 | 28 | parts := strings.Split(res.Header.Get("Location"), "/") 29 | id := parts[len(parts)-1] 30 | return id 31 | } 32 | 33 | func TestClientsService_Create(t *testing.T) { 34 | k := client(t) 35 | 36 | realm := "first" 37 | createRealm(t, k, realm) 38 | 39 | ctx := context.Background() 40 | 41 | client := &Client{ 42 | Enabled: Bool(true), 43 | ClientID: String("myclient"), 44 | } 45 | 46 | res, err := k.Clients.Create(ctx, realm, client) 47 | if err != nil { 48 | t.Errorf("Clients.Create returned error: %v", err) 49 | } 50 | 51 | if res.StatusCode != http.StatusCreated { 52 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 53 | } 54 | } 55 | 56 | func TestClientsService_List(t *testing.T) { 57 | k := client(t) 58 | 59 | realm := "first" 60 | createRealm(t, k, realm) 61 | 62 | clients, res, err := k.Clients.List(context.Background(), realm) 63 | if err != nil { 64 | t.Errorf("Clients.List returned error: %v", err) 65 | } 66 | 67 | if res.StatusCode != http.StatusOK { 68 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 69 | } 70 | 71 | // it includes the "account", "account-console", "admin-cli", "broker", "realm-management", "security-admin-console" 72 | if len(clients) != 6 { 73 | t.Errorf("got: %d, want: %d", len(clients), 6) 74 | } 75 | } 76 | 77 | func TestClientsService_Get(t *testing.T) { 78 | k := client(t) 79 | 80 | realm := "first" 81 | createRealm(t, k, realm) 82 | 83 | clientID := createClient(t, k, realm, "client") 84 | 85 | client, res, err := k.Clients.Get(context.Background(), realm, clientID) 86 | if err != nil { 87 | t.Errorf("Clients.Get returned error: %v", err) 88 | } 89 | 90 | if res.StatusCode != http.StatusOK { 91 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 92 | } 93 | 94 | if *client.ClientID != "client" { 95 | t.Errorf("got: %s, want: %s", *client.ClientID, "client") 96 | } 97 | } 98 | 99 | func TestClientsService_GetSecret(t *testing.T) { 100 | k := client(t) 101 | 102 | realm := "first" 103 | createRealm(t, k, realm) 104 | 105 | clientID := createClient(t, k, realm, "client") 106 | 107 | credential, res, err := k.Clients.GetSecret(context.Background(), realm, clientID) 108 | if err != nil { 109 | t.Errorf("Clients.Get returned error: %v", err) 110 | } 111 | 112 | if res.StatusCode != http.StatusOK { 113 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 114 | } 115 | 116 | if *credential.Type != "secret" { 117 | t.Errorf("got: %s, want: %s", *credential.Type, "secret") 118 | } 119 | } 120 | 121 | func TestClientsService_CreateSecret(t *testing.T) { 122 | k := client(t) 123 | 124 | realm := "first" 125 | createRealm(t, k, realm) 126 | 127 | clientID := createClient(t, k, realm, "client") 128 | 129 | ctx := context.Background() 130 | credential, res, err := k.Clients.GetSecret(ctx, realm, clientID) 131 | if err != nil { 132 | t.Errorf("Clients.Get returned error: %v", err) 133 | } 134 | 135 | if res.StatusCode != http.StatusOK { 136 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 137 | } 138 | 139 | if *credential.Type != "secret" { 140 | t.Errorf("got: %s, want: %s", *credential.Type, "secret") 141 | } 142 | 143 | // create a new secret 144 | next, _, err := k.Clients.CreateSecret(ctx, realm, clientID) 145 | if err != nil { 146 | t.Errorf("Clients.CreateSecret returned error: %v", err) 147 | } 148 | 149 | // make sure it is a different one 150 | if *next.Type != "secret" { 151 | t.Errorf("got: %s, want: %s", *next.Type, "secret") 152 | } 153 | 154 | if credential.Value == next.Value { 155 | t.Errorf("got: %t, want: %t", credential.Value == next.Value, credential.Value != next.Value) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /policies_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func createUserPolicy(t *testing.T, k *Keycloak, realm, clientID, userID string) *UserPolicy { 10 | policy := &UserPolicy{ 11 | Policy: Policy{ 12 | Type: String("user"), 13 | Logic: String(LogicPositive), 14 | DecisionStrategy: String(DecisionStrategyUnanimous), 15 | Name: String("policy"), 16 | }, 17 | Users: []string{userID}, 18 | } 19 | 20 | policy, _, err := k.Policies.CreateUserPolicy(context.Background(), realm, clientID, policy) 21 | if err != nil { 22 | t.Errorf("Policies.CreateUserPolicy returned error: %v", err) 23 | } 24 | return policy 25 | } 26 | 27 | func TestPoliciesService_CreateUserPolicy(t *testing.T) { 28 | k := client(t) 29 | 30 | realm := "first" 31 | createRealm(t, k, realm) 32 | clientID := createClient(t, k, realm, "client") 33 | userID := createUser(t, k, realm, "john") 34 | 35 | policy := &UserPolicy{ 36 | Policy: Policy{ 37 | Type: String("user"), 38 | Logic: String(LogicPositive), 39 | DecisionStrategy: String(DecisionStrategyUnanimous), 40 | Name: String("policy"), 41 | }, 42 | Users: []string{userID}, 43 | } 44 | 45 | policy, res, err := k.Policies.CreateUserPolicy(context.Background(), realm, clientID, policy) 46 | if err != nil { 47 | t.Errorf("Policies.CreateUserPolicy returned error: %v", err) 48 | } 49 | 50 | if res.StatusCode != http.StatusCreated { 51 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 52 | } 53 | 54 | if *policy.Name != "policy" { 55 | t.Errorf("got: %s, want: %s", *policy.Name, "policy") 56 | } 57 | } 58 | 59 | func TestPoliciesService_CreateRolePolicy(t *testing.T) { 60 | k := client(t) 61 | 62 | realm := "first" 63 | createRealm(t, k, realm) 64 | clientID := createClient(t, k, realm, "client") 65 | 66 | createRealmRole(t, k, realm, "role") 67 | 68 | role, _, err := k.RealmRoles.GetByName(context.Background(), realm, "role") 69 | if err != nil { 70 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 71 | } 72 | 73 | policy := &RolePolicy{ 74 | Policy: Policy{ 75 | Type: String("role"), 76 | Logic: String(LogicPositive), 77 | DecisionStrategy: String(DecisionStrategyUnanimous), 78 | Name: String("policy"), 79 | }, 80 | Roles: []*RoleDefinition{{ 81 | ID: role.ID, 82 | }}, 83 | } 84 | 85 | policy, res, err := k.Policies.CreateRolePolicy(context.Background(), realm, clientID, policy) 86 | if err != nil { 87 | t.Errorf("Policies.CreateRolePolicy returned error: %v", err) 88 | } 89 | 90 | if res.StatusCode != http.StatusCreated { 91 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 92 | } 93 | 94 | if *policy.Name != "policy" { 95 | t.Errorf("got: %s, want: %s", *policy.Name, "policy") 96 | } 97 | } 98 | 99 | func TestPoliciesService_CreateGroupPolicy(t *testing.T) { 100 | k := client(t) 101 | 102 | realm := "first" 103 | createRealm(t, k, realm) 104 | clientID := createClient(t, k, realm, "client") 105 | 106 | groupID := createGroup(t, k, realm, "group") 107 | 108 | policy := &GroupPolicy{ 109 | Policy: Policy{ 110 | Type: String("role"), 111 | Logic: String(LogicPositive), 112 | DecisionStrategy: String(DecisionStrategyUnanimous), 113 | Name: String("policy"), 114 | }, 115 | Groups: []*GroupDefinition{{ 116 | ID: &groupID, 117 | }}, 118 | } 119 | 120 | policy, res, err := k.Policies.CreateGroupPolicy(context.Background(), realm, clientID, policy) 121 | if err != nil { 122 | t.Errorf("Policies.CreateGroupPolicy returned error: %v", err) 123 | } 124 | 125 | if res.StatusCode != http.StatusCreated { 126 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 127 | } 128 | 129 | if *policy.Name != "policy" { 130 | t.Errorf("got: %s, want: %s", *policy.Name, "policy") 131 | } 132 | } 133 | 134 | func TestPoliciesService_List(t *testing.T) { 135 | k := client(t) 136 | 137 | realm := "first" 138 | createRealm(t, k, realm) 139 | clientID := createClient(t, k, realm, "client") 140 | 141 | roles, res, err := k.Policies.List(context.Background(), realm, clientID) 142 | if err != nil { 143 | t.Errorf("Policies.List returned error: %v", err) 144 | } 145 | 146 | if res.StatusCode != http.StatusOK { 147 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 148 | } 149 | 150 | if len(roles) != 1 { 151 | t.Errorf("got: %d, want: %d", len(roles), 1) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /groups_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // create a new group. 11 | func createGroup(t *testing.T, k *Keycloak, realm string, groupName string) string { 12 | t.Helper() 13 | 14 | group := &Group{ 15 | Name: String(groupName), 16 | } 17 | 18 | res, err := k.Groups.Create(context.Background(), realm, group) 19 | if err != nil { 20 | t.Errorf("Groups.Create returned error: %v", err) 21 | } 22 | 23 | parts := strings.Split(res.Header.Get("Location"), "/") 24 | groupID := parts[len(parts)-1] 25 | return groupID 26 | } 27 | 28 | func TestGroupsService_Create(t *testing.T) { 29 | k := client(t) 30 | 31 | realm := "first" 32 | createRealm(t, k, realm) 33 | 34 | ctx := context.Background() 35 | 36 | group := &Group{ 37 | Name: String("mygroup"), 38 | } 39 | 40 | res, err := k.Groups.Create(ctx, realm, group) 41 | if err != nil { 42 | t.Errorf("Groups.Create returned error: %v", err) 43 | } 44 | 45 | if res.StatusCode != http.StatusCreated { 46 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 47 | } 48 | } 49 | 50 | func TestGroupsService_List(t *testing.T) { 51 | k := client(t) 52 | 53 | realm := "first" 54 | createRealm(t, k, realm) 55 | 56 | createGroup(t, k, realm, "group_a") 57 | createGroup(t, k, realm, "group_b") 58 | 59 | groups, res, err := k.Groups.List(context.Background(), realm) 60 | if err != nil { 61 | t.Errorf("Groups.List returned error: %v", err) 62 | } 63 | 64 | if res.StatusCode != http.StatusOK { 65 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 66 | } 67 | 68 | if len(groups) != 2 { 69 | t.Errorf("got: %d, want: %d", len(groups), 2) 70 | } 71 | } 72 | 73 | func TestGroupsService_Get(t *testing.T) { 74 | k := client(t) 75 | 76 | realm := "first" 77 | createRealm(t, k, realm) 78 | 79 | groupID := createGroup(t, k, realm, "group") 80 | 81 | group, res, err := k.Groups.Get(context.Background(), realm, groupID) 82 | if err != nil { 83 | t.Errorf("Groups.Get returned error: %v", err) 84 | } 85 | 86 | if res.StatusCode != http.StatusOK { 87 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 88 | } 89 | 90 | if *group.Name != "group" { 91 | t.Errorf("got: %s, want: %s", *group.Name, "group") 92 | } 93 | } 94 | 95 | func TestGroupsService_Delete(t *testing.T) { 96 | k := client(t) 97 | 98 | realm := "first" 99 | createRealm(t, k, realm) 100 | 101 | groupID := createGroup(t, k, realm, "group") 102 | 103 | res, err := k.Groups.Delete(context.Background(), realm, groupID) 104 | if err != nil { 105 | t.Errorf("Groups.Delete returned error: %v", err) 106 | } 107 | 108 | if res.StatusCode != http.StatusNoContent { 109 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 110 | } 111 | } 112 | 113 | func TestGroupsService_AddRealmRoles(t *testing.T) { 114 | k := client(t) 115 | 116 | realm := "first" 117 | roleNames := [2]string{"role1", "role2"} 118 | 119 | createRealm(t, k, realm) 120 | for _, r := range roleNames { 121 | createRealmRole(t, k, realm, r) 122 | } 123 | groupID := createGroup(t, k, realm, "group") 124 | 125 | ctx := context.Background() 126 | 127 | // get role in order to assign it 128 | var roles []*Role 129 | for _, r := range roleNames { 130 | role, _, err := k.RealmRoles.GetByName(ctx, realm, r) 131 | if err != nil { 132 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 133 | } 134 | roles = append(roles, role) 135 | } 136 | 137 | res, err := k.Groups.AddRealmRoles(ctx, realm, groupID, roles) 138 | if err != nil { 139 | t.Errorf("Groups.AddRealmRoles returned error: %v", err) 140 | } 141 | 142 | if res.StatusCode != http.StatusNoContent { 143 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 144 | } 145 | } 146 | 147 | func TestGroupsService_RemoveRealmRoles(t *testing.T) { 148 | k := client(t) 149 | 150 | realm := "first" 151 | roleNames := [2]string{"role1", "role2"} 152 | 153 | createRealm(t, k, realm) 154 | for _, r := range roleNames { 155 | createRealmRole(t, k, realm, r) 156 | } 157 | groupID := createGroup(t, k, realm, "group") 158 | 159 | ctx := context.Background() 160 | 161 | // get role in order to assign it 162 | var roles []*Role 163 | for _, r := range roleNames { 164 | role, _, err := k.RealmRoles.GetByName(ctx, realm, r) 165 | if err != nil { 166 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 167 | } 168 | roles = append(roles, role) 169 | } 170 | 171 | _, err := k.Groups.AddRealmRoles(ctx, realm, groupID, roles) 172 | if err != nil { 173 | t.Errorf("Groups.AddRealmRoles returned error: %v", err) 174 | } 175 | 176 | res, err := k.Groups.RemoveRealmRoles(ctx, realm, groupID, roles) 177 | if err != nil { 178 | t.Errorf("Groups.RemoveRealmRoles returned error: %v", err) 179 | } 180 | 181 | if res.StatusCode != http.StatusNoContent { 182 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /permissions.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // PermissionsService handles communication with the permissions related methods of the Keycloak API. 10 | type PermissionsService service 11 | 12 | // Permission represents a Keycloak abstract permission. 13 | // 14 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java 15 | type Permission struct { 16 | ID *string `json:"id,omitempty"` 17 | Name *string `json:"name,omitempty"` 18 | Description *string `json:"description,omitempty"` 19 | Type *string `json:"type,omitempty"` 20 | Policies []string `json:"policies,omitempty"` 21 | Resources []string `json:"resources,omitempty"` 22 | Scopes []string `json:"scopes,omitempty"` 23 | Logic *string `json:"logic,omitempty"` 24 | DecisionStrategy *string `json:"decisionStrategy,omitempty"` 25 | Owner *string `json:"owner,omitempty"` 26 | } 27 | 28 | // ResourcePermission represents a Keycloak resource permission. 29 | // 30 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/ResourcePermissionRepresentation.java 31 | type ResourcePermission struct { 32 | Permission 33 | ResourceType *string `json:"resourceType,omitempty"` 34 | } 35 | 36 | // ScopePermission represents a Keycloak scope permission. 37 | // 38 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java 39 | type ScopePermission struct { 40 | Permission 41 | ResourceType *string `json:"resourceType,omitempty"` 42 | } 43 | 44 | // List lists all permissions. 45 | func (s *PermissionsService) List(ctx context.Context, realm, clientID string) ([]*Permission, *http.Response, error) { 46 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/permission", realm, clientID) 47 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 48 | if err != nil { 49 | return nil, nil, err 50 | } 51 | 52 | var permission []*Permission 53 | res, err := s.keycloak.Do(ctx, req, &permission) 54 | if err != nil { 55 | return nil, nil, err 56 | } 57 | 58 | return permission, res, nil 59 | } 60 | 61 | // CreateResourcePermission creates a new resource based permission. 62 | func (s *PermissionsService) CreateResourcePermission(ctx context.Context, realm, clientID string, permission *ResourcePermission) (*ResourcePermission, *http.Response, error) { 63 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/permission/resource", realm, clientID) 64 | req, err := s.keycloak.NewRequest(http.MethodPost, u, permission) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | var created ResourcePermission 70 | res, err := s.keycloak.Do(ctx, req, &created) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | 75 | return &created, res, nil 76 | } 77 | 78 | // GetResourcePermission gets resource based permission by id. 79 | func (s *PermissionsService) GetResourcePermission(ctx context.Context, realm, clientID, permissionID string) (*ResourcePermission, *http.Response, error) { 80 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/permission/resource/%s", realm, clientID, permissionID) 81 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | var permission ResourcePermission 87 | res, err := s.keycloak.Do(ctx, req, &permission) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | 92 | return &permission, res, nil 93 | } 94 | 95 | // GetScopePermission gets scope based permission by id. 96 | func (s *PermissionsService) GetScopePermission(ctx context.Context, realm, clientID, permissionID string) (*ScopePermission, *http.Response, error) { 97 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/permission/scope/%s", realm, clientID, permissionID) 98 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 99 | if err != nil { 100 | return nil, nil, err 101 | } 102 | 103 | var permission ScopePermission 104 | res, err := s.keycloak.Do(ctx, req, &permission) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | 109 | return &permission, res, nil 110 | } 111 | 112 | // CreateScopePermission creates a new scope based permission. 113 | func (s *PermissionsService) CreateScopePermission(ctx context.Context, realm, clientID string, permission *ScopePermission) (*ScopePermission, *http.Response, error) { 114 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/permission/scope", realm, clientID) 115 | req, err := s.keycloak.NewRequest(http.MethodPost, u, permission) 116 | if err != nil { 117 | return nil, nil, err 118 | } 119 | 120 | var created ScopePermission 121 | res, err := s.keycloak.Do(ctx, req, &created) 122 | if err != nil { 123 | return nil, nil, err 124 | } 125 | 126 | return &created, res, nil 127 | } 128 | -------------------------------------------------------------------------------- /policies.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // PoliciesService handles communication with the policies related methods of the Keycloak API. 10 | type PoliciesService service 11 | 12 | // Policy represents a Keycloak abstract policy. 13 | // 14 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java 15 | type Policy struct { 16 | ID *string `json:"id,omitempty"` 17 | Name *string `json:"name,omitempty"` 18 | Description *string `json:"description,omitempty"` 19 | Type *string `json:"type,omitempty"` 20 | Policies []string `json:"policies,omitempty"` 21 | Resources []string `json:"resources,omitempty"` 22 | Scopes []string `json:"scopes,omitempty"` 23 | Logic *string `json:"logic,omitempty"` 24 | DecisionStrategy *string `json:"decisionStrategy,omitempty"` 25 | Owner *string `json:"owner,omitempty"` 26 | } 27 | 28 | // GroupDefinition represents a Keycloak groupDefinition. 29 | // 30 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java 31 | type GroupDefinition struct { 32 | ID *string `json:"id,omitempty"` 33 | Path *string `json:"path,omitempty"` 34 | ExtendChildren *bool `json:"extendChildren,omitempty"` 35 | } 36 | 37 | // GroupPolicy represents a Keycloak group policy. 38 | // 39 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java 40 | type GroupPolicy struct { 41 | Policy 42 | GroupsClaim *string `json:"groupsClaim,omitempty"` 43 | Groups []*GroupDefinition `json:"groups,omitempty"` 44 | } 45 | 46 | // UserPolicy represents a Keycloak user policy. 47 | // 48 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/UserPolicyRepresentation.java 49 | type UserPolicy struct { 50 | Policy 51 | Users []string `json:"users,omitempty"` 52 | } 53 | 54 | // RolePolicy represents a Keycloak role policy. 55 | // 56 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java 57 | type RolePolicy struct { 58 | Policy 59 | Roles []*RoleDefinition `json:"roles,omitempty"` 60 | } 61 | 62 | // RoleDefinition represents a Keycloak role definition. 63 | // 64 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java 65 | type RoleDefinition struct { 66 | ID *string `json:"id,omitempty"` 67 | Required *bool `json:"required,omitempty"` 68 | } 69 | 70 | // List lists all policies. 71 | func (s *PoliciesService) List(ctx context.Context, realm, clientID string) ([]*Policy, *http.Response, error) { 72 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/policy?permission=false", realm, clientID) 73 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | 78 | var policies []*Policy 79 | res, err := s.keycloak.Do(ctx, req, &policies) 80 | if err != nil { 81 | return nil, nil, err 82 | } 83 | 84 | return policies, res, nil 85 | } 86 | 87 | // CreateUserPolicy creates a new user policy. 88 | func (s *PoliciesService) CreateUserPolicy(ctx context.Context, realm, clientID string, policy *UserPolicy) (*UserPolicy, *http.Response, error) { 89 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/policy/user", realm, clientID) 90 | req, err := s.keycloak.NewRequest(http.MethodPost, u, policy) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | var created UserPolicy 96 | res, err := s.keycloak.Do(ctx, req, &created) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | 101 | return &created, res, nil 102 | } 103 | 104 | // CreateRolePolicy creates a new role policy. 105 | func (s *PoliciesService) CreateRolePolicy(ctx context.Context, realm, clientID string, policy *RolePolicy) (*RolePolicy, *http.Response, error) { 106 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/policy/role", realm, clientID) 107 | req, err := s.keycloak.NewRequest(http.MethodPost, u, policy) 108 | if err != nil { 109 | return nil, nil, err 110 | } 111 | 112 | var created RolePolicy 113 | res, err := s.keycloak.Do(ctx, req, &created) 114 | if err != nil { 115 | return nil, nil, err 116 | } 117 | 118 | return &created, res, nil 119 | } 120 | 121 | // CreateGroupPolicy creates a new group policy. 122 | func (s *PoliciesService) CreateGroupPolicy(ctx context.Context, realm, clientID string, policy *GroupPolicy) (*GroupPolicy, *http.Response, error) { 123 | u := fmt.Sprintf("admin/realms/%s/clients/%s/authz/resource-server/policy/group", realm, clientID) 124 | req, err := s.keycloak.NewRequest(http.MethodPost, u, policy) 125 | if err != nil { 126 | return nil, nil, err 127 | } 128 | 129 | var created GroupPolicy 130 | res, err := s.keycloak.Do(ctx, req, &created) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | 135 | return &created, res, nil 136 | } 137 | -------------------------------------------------------------------------------- /clients.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ClientsService handles communication with the client related methods of the Keycloak API. 10 | type ClientsService service 11 | 12 | // Client represents a Keycloak client. 13 | // 14 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java 15 | type Client struct { 16 | ID *string `json:"id,omitempty"` 17 | ClientID *string `json:"clientId,omitempty"` 18 | Name *string `json:"name,omitempty"` 19 | RootURL *string `json:"rootUrl,omitempty"` 20 | BaseURL *string `json:"baseUrl,omitempty"` 21 | SurrogateAuthRequired *bool `json:"surrogateAuthRequired,omitempty"` 22 | Enabled *bool `json:"enabled,omitempty"` 23 | AlwaysDisplayInConsole *bool `json:"alwaysDisplayInConsole,omitempty"` 24 | ClientAuthenticatorType *string `json:"clientAuthenticatorType,omitempty"` 25 | DefaultRoles []string `json:"defaultRoles,omitempty"` 26 | RedirectUris []string `json:"redirectUris,omitempty"` 27 | WebOrigins []string `json:"webOrigins,omitempty"` 28 | NotBefore *int `json:"notBefore,omitempty"` 29 | BearerOnly *bool `json:"bearerOnly,omitempty"` 30 | ConsentRequired *bool `json:"consentRequired,omitempty"` 31 | StandardFlowEnabled *bool `json:"standardFlowEnabled,omitempty"` 32 | ImplicitFlowEnabled *bool `json:"implicitFlowEnabled,omitempty"` 33 | DirectAccessGrantsEnabled *bool `json:"directAccessGrantsEnabled,omitempty"` 34 | ServiceAccountsEnabled *bool `json:"serviceAccountsEnabled,omitempty"` 35 | AuthorizationServicesEnabled *bool `json:"authorizationServicesEnabled,omitempty"` 36 | PublicClient *bool `json:"publicClient,omitempty"` 37 | FrontchannelLogout *bool `json:"frontchannelLogout,omitempty"` 38 | Protocol *string `json:"protocol,omitempty"` 39 | Attributes *map[string]string `json:"attributes,omitempty"` 40 | AuthenticationFlowBindingOverrides *map[string]string `json:"authenticationFlowBindingOverrides,omitempty"` 41 | FullScopeAllowed *bool `json:"fullScopeAllowed,omitempty"` 42 | NodeReRegistrationTimeout *int `json:"nodeReRegistrationTimeout,omitempty"` 43 | DefaultClientScopes []string `json:"defaultClientScopes,omitempty"` 44 | OptionalClientScopes []string `json:"optionalClientScopes,omitempty"` 45 | Access *map[string]bool `json:"access,omitempty"` 46 | } 47 | 48 | // List all clients in realm. 49 | func (s *ClientsService) List(ctx context.Context, realm string) ([]*Client, *http.Response, error) { 50 | u := fmt.Sprintf("admin/realms/%s/clients", realm) 51 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | 56 | var clients []*Client 57 | res, err := s.keycloak.Do(ctx, req, &clients) 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | return clients, res, nil 63 | } 64 | 65 | // Create a new client. 66 | func (s *ClientsService) Create(ctx context.Context, realm string, client *Client) (*http.Response, error) { 67 | u := fmt.Sprintf("admin/realms/%s/clients", realm) 68 | req, err := s.keycloak.NewRequest(http.MethodPost, u, client) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return s.keycloak.Do(ctx, req, nil) 74 | } 75 | 76 | // Update a new client. 77 | func (s *ClientsService) Update(ctx context.Context, realm string, client *Client) (*http.Response, error) { 78 | u := fmt.Sprintf("admin/realms/%s/clients/%s", realm, *client.ID) 79 | req, err := s.keycloak.NewRequest(http.MethodPut, u, client) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return s.keycloak.Do(ctx, req, nil) 85 | } 86 | 87 | // Get client. 88 | func (s *ClientsService) Get(ctx context.Context, realm, id string) (*Client, *http.Response, error) { 89 | u := fmt.Sprintf("admin/realms/%s/clients/%s", realm, id) 90 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | var client Client 96 | res, err := s.keycloak.Do(ctx, req, &client) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | 101 | return &client, res, nil 102 | } 103 | 104 | // Delete ... 105 | func (s *ClientsService) Delete() { 106 | 107 | } 108 | 109 | // GetSecret gets client secret. 110 | func (s *ClientsService) GetSecret(ctx context.Context, realm, id string) (*Credential, *http.Response, error) { 111 | u := fmt.Sprintf("admin/realms/%s/clients/%s/client-secret", realm, id) 112 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 113 | if err != nil { 114 | return nil, nil, err 115 | } 116 | 117 | var credential Credential 118 | res, err := s.keycloak.Do(ctx, req, &credential) 119 | if err != nil { 120 | return nil, nil, err 121 | } 122 | 123 | return &credential, res, nil 124 | } 125 | 126 | // CreateSecret generates a new secret for the client 127 | func (s *ClientsService) CreateSecret(ctx context.Context, realm, id string) (*Credential, *http.Response, error) { 128 | u := fmt.Sprintf("admin/realms/%s/clients/%s/client-secret", realm, id) 129 | req, err := s.keycloak.NewRequest(http.MethodPost, u, nil) 130 | if err != nil { 131 | return nil, nil, err 132 | } 133 | 134 | var credential Credential 135 | res, err := s.keycloak.Do(ctx, req, &credential) 136 | if err != nil { 137 | return nil, nil, err 138 | } 139 | 140 | return &credential, res, nil 141 | } 142 | 143 | // Options ... 144 | type Options struct { 145 | First int `url:"first,omitempty"` 146 | Max string `url:"max,omitempty"` 147 | } 148 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package keycloak_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/zemirco/keycloak/v2" 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | func ExampleNewKeycloak_admin() { 12 | // create a new oauth2 configuration 13 | config := oauth2.Config{ 14 | ClientID: "admin-cli", 15 | Endpoint: oauth2.Endpoint{ 16 | TokenURL: "http://localhost:8080/realms/master/protocol/openid-connect/token", 17 | }, 18 | } 19 | 20 | ctx := context.Background() 21 | 22 | // this should be your KEYCLOAK_USER and your KEYCLOAK_PASSWORD 23 | token, err := config.PasswordCredentialsToken(ctx, "admin", "admin") 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | // create a new http client 29 | httpClient := config.Client(ctx, token) 30 | 31 | // use the http client to create a Keycloak instance 32 | kc, err := keycloak.NewKeycloak(httpClient, "http://localhost:8080/") 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // then use this instance to make requests to the API 38 | fmt.Println(kc) 39 | } 40 | 41 | func ExampleNewKeycloak_user() { 42 | // create a new oauth2 configuration 43 | config := oauth2.Config{ 44 | ClientID: "40349713-a521-48b2-9197-216adfce5f78", 45 | ClientSecret: "ddafbeba-1402-4969-bbbb-475d657fa9d5", 46 | Endpoint: oauth2.Endpoint{ 47 | TokenURL: fmt.Sprintf("http://localhost:8080/realms/%s/protocol/openid-connect/token", "myrealm"), 48 | }, 49 | } 50 | 51 | ctx := context.Background() 52 | 53 | // this should be your username and your password 54 | token, err := config.PasswordCredentialsToken(ctx, "user", "user_password") 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | // create a new http client 60 | httpClient := config.Client(ctx, token) 61 | 62 | // use the http client to create a Keycloak instance 63 | kc, err := keycloak.NewKeycloak(httpClient, "http://localhost:8080/") 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | // then use this instance to make requests to the API 69 | fmt.Println(kc) 70 | } 71 | 72 | func ExampleRealmsService_Create() { 73 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | realm := &keycloak.Realm{ 79 | Enabled: keycloak.Bool(true), 80 | ID: keycloak.String("myrealm"), 81 | Realm: keycloak.String("myrealm"), 82 | } 83 | 84 | ctx := context.Background() 85 | 86 | res, err := kc.Realms.Create(ctx, realm) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | fmt.Println(res) 92 | } 93 | 94 | func ExampleUsersService_Create() { 95 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | user := &keycloak.User{ 101 | Enabled: keycloak.Bool(true), 102 | Username: keycloak.String("username"), 103 | Email: keycloak.String("user@email.com"), 104 | FirstName: keycloak.String("first"), 105 | LastName: keycloak.String("last"), 106 | } 107 | 108 | ctx := context.Background() 109 | res, err := kc.Users.Create(ctx, "myrealm", user) 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | fmt.Println(res) 115 | } 116 | 117 | func ExampleClientsService_Create() { 118 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | client := &keycloak.Client{ 124 | Enabled: keycloak.Bool(true), 125 | ClientID: keycloak.String("myclient"), 126 | } 127 | 128 | ctx := context.Background() 129 | res, err := kc.Clients.Create(ctx, "myrealm", client) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | fmt.Println(res) 135 | } 136 | 137 | func ExampleScopesService_Create() { 138 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 139 | if err != nil { 140 | panic(err) 141 | } 142 | 143 | scope := &keycloak.Scope{ 144 | Name: keycloak.String("scope_read"), 145 | } 146 | 147 | ctx := context.Background() 148 | 149 | clientID := "40349713-a521-48b2-9197-216adfce5f78" 150 | scope, res, err := kc.Scopes.Create(ctx, "myrealm", clientID, scope) 151 | if err != nil { 152 | panic(err) 153 | } 154 | 155 | fmt.Println(res) 156 | } 157 | 158 | func ExampleRealmRolesService_Create() { 159 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 160 | if err != nil { 161 | panic(err) 162 | } 163 | 164 | role := &keycloak.Role{ 165 | Name: keycloak.String("my name"), 166 | Description: keycloak.String("my description"), 167 | } 168 | 169 | ctx := context.Background() 170 | res, err := kc.RealmRoles.Create(ctx, "myrealm", role) 171 | if err != nil { 172 | panic(err) 173 | } 174 | 175 | fmt.Println(res) 176 | } 177 | 178 | func ExampleResourcesService_Create() { 179 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 180 | if err != nil { 181 | panic(err) 182 | } 183 | 184 | resource := &keycloak.Resource{ 185 | Name: keycloak.String("resource"), 186 | DisplayName: keycloak.String("resource"), 187 | } 188 | 189 | ctx := context.Background() 190 | 191 | clientID := "40349713-a521-48b2-9197-216adfce5f78" 192 | resource, res, err := kc.Resources.Create(ctx, "myrealm", clientID, resource) 193 | if err != nil { 194 | panic(err) 195 | } 196 | 197 | fmt.Println(res) 198 | } 199 | 200 | func ExamplePoliciesService_CreateUserPolicy() { 201 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 202 | if err != nil { 203 | panic(err) 204 | } 205 | 206 | policy := &keycloak.UserPolicy{ 207 | Policy: keycloak.Policy{ 208 | Type: keycloak.String("user"), 209 | Logic: keycloak.String(keycloak.LogicPositive), 210 | DecisionStrategy: keycloak.String(keycloak.DecisionStrategyUnanimous), 211 | Name: keycloak.String("policy"), 212 | }, 213 | Users: []string{"89400c55-15fd-4e5d-a7c9-403f431d97f3"}, 214 | } 215 | 216 | ctx := context.Background() 217 | 218 | clientID := "40349713-a521-48b2-9197-216adfce5f78" 219 | policy, res, err := kc.Policies.CreateUserPolicy(ctx, "myrealm", clientID, policy) 220 | if err != nil { 221 | panic(err) 222 | } 223 | 224 | fmt.Println(res) 225 | } 226 | 227 | func ExampleGroupsService_Create() { 228 | kc, err := keycloak.NewKeycloak(nil, "http://localhost:8080/") 229 | if err != nil { 230 | panic(err) 231 | } 232 | 233 | group := &keycloak.Group{ 234 | Name: keycloak.String("mygroup"), 235 | } 236 | 237 | ctx := context.Background() 238 | res, err := kc.Groups.Create(ctx, "myrealm", group) 239 | if err != nil { 240 | panic(err) 241 | } 242 | 243 | fmt.Println(res) 244 | } 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # keycloak 3 | 4 | [![ci](https://github.com/zemirco/keycloak/workflows/ci/badge.svg)](https://github.com/zemirco/keycloak/actions/workflows/ci.yml) 5 | [![codeql](https://github.com/zemirco/keycloak/workflows/codeql/badge.svg)](https://github.com/zemirco/keycloak/actions/workflows/codeql.yml) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/zemirco/keycloak.svg)](https://pkg.go.dev/github.com/zemirco/keycloak) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/zemirco/keycloak)](https://goreportcard.com/report/github.com/zemirco/keycloak) 8 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/zemirco/keycloak.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zemirco/keycloak/alerts/) 9 | 10 | keycloak is a Go client library for accessing the [Keycloak API](https://www.keycloak.org/docs-api/12.0/rest-api/index.html). 11 | 12 | ## Installation 13 | 14 | ```bash 15 | go get github.com/zemirco/keycloak/v2 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/zemirco/keycloak/v2" 27 | "golang.org/x/oauth2" 28 | ) 29 | 30 | func main() { 31 | // create your oauth configuration 32 | config := oauth2.Config{ 33 | ClientID: "admin-cli", 34 | Endpoint: oauth2.Endpoint{ 35 | TokenURL: "http://localhost:8080/realms/master/protocol/openid-connect/token", 36 | }, 37 | } 38 | 39 | // get a valid token from keycloak 40 | ctx := context.Background() 41 | token, err := config.PasswordCredentialsToken(ctx, "admin", "admin") 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | // create a new http client that uses the token on every request 47 | client := config.Client(ctx, token) 48 | 49 | // create a new keycloak instance and provide the http client 50 | k, err := keycloak.NewKeycloak(client, "http://localhost:8080/") 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | // start using the library and, for example, create a new realm 56 | realm := &keycloak.Realm{ 57 | Enabled: keycloak.Bool(true), 58 | ID: keycloak.String("myrealm"), 59 | Realm: keycloak.String("myrealm"), 60 | } 61 | 62 | res, err := k.Realms.Create(ctx, realm) 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | ``` 68 | 69 | ## Examples 70 | 71 | - [full example](https://github.com/zemirco/keycloak/blob/main/example_full_test.go): realm, client, users, resources, policies, permissions, evaluation 72 | - [ClientsService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-ClientsService.Create): Create a new client 73 | - [GroupsService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-GroupsService.Create): Create a new group 74 | - [NewKeycloak-Admin](https://pkg.go.dev/github.com/zemirco/keycloak#example-NewKeycloak-Admin): Create an admin instance 75 | - [NewKeycloak-User](https://pkg.go.dev/github.com/zemirco/keycloak#example-NewKeycloak-User): Create a user instance 76 | - [PoliciesService.CreateUserPolicy](https://pkg.go.dev/github.com/zemirco/keycloak#example-PoliciesService.CreateUserPolicy): Create a user policy 77 | - [RealmsService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-RealmsService.Create): Create a new realm 78 | - [ResourcesService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-ResourcesService.Create): Create a new resource 79 | - [RolesService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-RolesService.Create): Create a new role 80 | - [ScopesService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-ScopesService.Create): Create a new scope 81 | - [UsersService.Create](https://pkg.go.dev/github.com/zemirco/keycloak#example-UsersService.Create): Create a new user 82 | 83 | ## Development 84 | 85 | Use `docker-compose` to start Keycloak locally. 86 | 87 | ```bash 88 | docker-compose up -d 89 | ``` 90 | 91 | Keycloak is running at http://localhost:8080/. The admin credentials are `admin` (username) and `admin` (password). If you want to change them simply edit the `docker-compose.yml`. 92 | 93 | Keycloak uses PostgreSQL and all data is kept across restarts. 94 | 95 | Use `down` if you want to stop the Keycloak server. 96 | 97 | ```bash 98 | docker-compose down 99 | ``` 100 | 101 | ## Architecture 102 | 103 | The main entry point is `keycloak.go`. This is where the Keycloak instance is created. It all starts in this file. 104 | 105 | Within Keycloak we also have the concept of clients. They are the ones that connect to Keycloak for authentication and authorization purposes, e.g. our frontend and backend apps. That is why this library simply uses the `keycloak` instance of type `Keycloak` and not a `client` instance like [go-github](https://github.com/google/go-github). Although technically this library is a Keycloak client library for Go. However this distinction should make it clear what is meant when we talk about a client in our context. 106 | 107 | ## Testing 108 | 109 | You need to have Keycloak running on your local machine to execute the tests. Simply use `docker-compose` to start it. 110 | 111 | All tests are independent from each other. Before each test we create a realm and after each test we delete it. You don't have to worry about it since the helper function `createRealm` does that automatically for you. Inside this realm you can do whatever you want. You don't have to clean up after yourself since everything is deleted automatically when the realm is deleted. 112 | 113 | Run all tests. 114 | 115 | ```bash 116 | go test -race -v ./... 117 | ``` 118 | 119 | Create code coverage. 120 | 121 | ```bash 122 | go test -v ./... -coverprofile=coverage.out 123 | go tool cover -html=coverage.out -o coverage.html 124 | ``` 125 | 126 | We have also provided a simple `Makefile` that run both jobs automatically. 127 | 128 | ```bash 129 | make 130 | ``` 131 | 132 | Open `coverage.html` with your browser. 133 | 134 | ## Design goals 135 | 136 | 1. Zero dependencies 137 | 138 | It's just the Go standard library. 139 | 140 | The only exception is [go-querystring](https://github.com/google/go-querystring) to easily handle query parameters. 141 | 142 | 1. Idiomatic Go 143 | 144 | Modelled after [go-github](https://github.com/google/go-github) and [go-jira](https://github.com/andygrunwald/go-jira). 145 | 146 | 1. Keep authentication outside this library 147 | 148 | This is the major difference to most of the other Go Keycloak libraries. 149 | 150 | We leverage the brilliant [oauth2](https://github.com/golang/oauth2) package to deal with authentication. We have provided multiple examples to show you the workflow. It basically means we do not provide any methods to call the `/token` endpoint. 151 | 152 | 1. Return struct and HTTP response 153 | 154 | Whenever the Keycloak API returns JSON content you'll get a proper struct as well as the HTTP response. 155 | 156 | ```go 157 | func (s *ClientsService) Get(ctx context.Context, realm, id string) (*Client, *http.Response, error) 158 | ``` 159 | 160 | ## Related work 161 | 162 | - https://github.com/Nerzal/gocloak 163 | - https://github.com/PhilippHeuer/go-keycloak 164 | - https://github.com/coreos/go-oidc 165 | - https://github.com/keycloak/kcinit 166 | - https://github.com/pulumi/pulumi-keycloak/tree/master/sdk/go/keycloak 167 | - https://github.com/airmap/go-keycloak 168 | - https://github.com/cloudtrust/keycloak-client 169 | - https://github.com/myENA/go-keycloak 170 | - https://github.com/threez/go-keycloak 171 | 172 | ## License 173 | 174 | MIT 175 | -------------------------------------------------------------------------------- /permissions_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestPermissionsService_CreateResourcePermission(t *testing.T) { 10 | k := client(t) 11 | 12 | realm := "first" 13 | createRealm(t, k, realm) 14 | clientID := createClient(t, k, realm, "client") 15 | userID := createUser(t, k, realm, "john") 16 | policy := createUserPolicy(t, k, realm, clientID, userID) 17 | resource := createResource(t, k, realm, clientID, "resource") 18 | 19 | // create first permission 20 | permission := &ResourcePermission{ 21 | Permission: Permission{ 22 | Type: String("resource"), 23 | Logic: String(LogicPositive), 24 | DecisionStrategy: String(DecisionStrategyUnanimous), 25 | Name: String("permission"), 26 | Resources: []string{*resource.ID}, 27 | Policies: []string{*policy.ID}, 28 | }, 29 | ResourceType: String(""), 30 | } 31 | permission, res, err := k.Permissions.CreateResourcePermission(context.Background(), realm, clientID, permission) 32 | if err != nil { 33 | t.Errorf("Permissions.CreateResourcePermission returned error: %v", err) 34 | } 35 | 36 | if res.StatusCode != http.StatusCreated { 37 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 38 | } 39 | 40 | if *permission.Name != "permission" { 41 | t.Errorf("got: %s, want: %s", *permission.Name, "permission") 42 | } 43 | } 44 | 45 | func TestPermissionsService_GetResourcePermission(t *testing.T) { 46 | k := client(t) 47 | 48 | realm := "first" 49 | createRealm(t, k, realm) 50 | clientID := createClient(t, k, realm, "client") 51 | userID := createUser(t, k, realm, "john") 52 | policy := createUserPolicy(t, k, realm, clientID, userID) 53 | resource := createResource(t, k, realm, clientID, "resource") 54 | 55 | // create first permission 56 | permission := &ResourcePermission{ 57 | Permission: Permission{ 58 | Type: String("resource"), 59 | Logic: String(LogicPositive), 60 | DecisionStrategy: String(DecisionStrategyUnanimous), 61 | Name: String("permission"), 62 | Resources: []string{*resource.ID}, 63 | Policies: []string{*policy.ID}, 64 | }, 65 | ResourceType: String(""), 66 | } 67 | permission, res, err := k.Permissions.CreateResourcePermission(context.Background(), realm, clientID, permission) 68 | if err != nil { 69 | t.Errorf("Permissions.CreateResourcePermission returned error: %v", err) 70 | } 71 | 72 | if res.StatusCode != http.StatusCreated { 73 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 74 | } 75 | 76 | permission, res, err = k.Permissions.GetResourcePermission(context.Background(), realm, clientID, *permission.ID) 77 | if err != nil { 78 | t.Errorf("Permissions.GetResourcePermission returned error: %v", err) 79 | } 80 | 81 | if res.StatusCode != http.StatusOK { 82 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 83 | } 84 | 85 | if *permission.Name != "permission" { 86 | t.Errorf("got: %s, want: %s", *permission.Name, "permission") 87 | } 88 | } 89 | 90 | func TestPermissionsService_CreateScopePermission(t *testing.T) { 91 | k := client(t) 92 | 93 | realm := "first" 94 | createRealm(t, k, realm) 95 | clientID := createClient(t, k, realm, "client") 96 | userID := createUser(t, k, realm, "john") 97 | policy := createUserPolicy(t, k, realm, clientID, userID) 98 | resource := createResource(t, k, realm, clientID, "resource") 99 | scope := createScope(t, k, realm, clientID, "scope") 100 | 101 | // create first permission 102 | permission := &ScopePermission{ 103 | Permission: Permission{ 104 | Type: String("resource"), 105 | Logic: String(LogicPositive), 106 | DecisionStrategy: String(DecisionStrategyUnanimous), 107 | Name: String("permission"), 108 | Resources: []string{*resource.ID}, 109 | Policies: []string{*policy.ID}, 110 | Scopes: []string{*scope.ID}, 111 | }, 112 | ResourceType: String(""), 113 | } 114 | permission, res, err := k.Permissions.CreateScopePermission(context.Background(), realm, clientID, permission) 115 | if err != nil { 116 | t.Errorf("Permissions.CreateScopePermission returned error: %v", err) 117 | } 118 | 119 | if res.StatusCode != http.StatusCreated { 120 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 121 | } 122 | 123 | if *permission.Name != "permission" { 124 | t.Errorf("got: %s, want: %s", *permission.Name, "permission") 125 | } 126 | } 127 | 128 | func TestPermissionsService_GetScopePermission(t *testing.T) { 129 | k := client(t) 130 | 131 | ctx := context.Background() 132 | 133 | realm := "first" 134 | createRealm(t, k, realm) 135 | clientID := createClient(t, k, realm, "client") 136 | userID := createUser(t, k, realm, "john") 137 | policy := createUserPolicy(t, k, realm, clientID, userID) 138 | resource := createResource(t, k, realm, clientID, "resource") 139 | scope := createScope(t, k, realm, clientID, "scope") 140 | 141 | // create first permission 142 | permission := &ScopePermission{ 143 | Permission: Permission{ 144 | Type: String("resource"), 145 | Logic: String(LogicPositive), 146 | DecisionStrategy: String(DecisionStrategyUnanimous), 147 | Name: String("permission"), 148 | Resources: []string{*resource.ID}, 149 | Policies: []string{*policy.ID}, 150 | Scopes: []string{*scope.ID}, 151 | }, 152 | ResourceType: String(""), 153 | } 154 | permission, res, err := k.Permissions.CreateScopePermission(ctx, realm, clientID, permission) 155 | if err != nil { 156 | t.Errorf("Permissions.CreateScopePermission returned error: %v", err) 157 | } 158 | 159 | if res.StatusCode != http.StatusCreated { 160 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 161 | } 162 | 163 | permission, res, err = k.Permissions.GetScopePermission(ctx, realm, clientID, *permission.ID) 164 | if err != nil { 165 | t.Errorf("Permissions.GetResourcePermission returned error: %v", err) 166 | } 167 | 168 | if res.StatusCode != http.StatusOK { 169 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 170 | } 171 | 172 | if *permission.Name != "permission" { 173 | t.Errorf("got: %s, want: %s", *permission.Name, "permission") 174 | } 175 | } 176 | 177 | func TestPermissionsService_List(t *testing.T) { 178 | k := client(t) 179 | 180 | ctx := context.Background() 181 | 182 | realm := "first" 183 | createRealm(t, k, realm) 184 | clientID := createClient(t, k, realm, "client") 185 | userID := createUser(t, k, realm, "john") 186 | policy := createUserPolicy(t, k, realm, clientID, userID) 187 | resource := createResource(t, k, realm, clientID, "resource") 188 | scope := createScope(t, k, realm, clientID, "scope") 189 | 190 | // create first permission 191 | permission := &ScopePermission{ 192 | Permission: Permission{ 193 | Type: String("resource"), 194 | Logic: String(LogicPositive), 195 | DecisionStrategy: String(DecisionStrategyUnanimous), 196 | Name: String("permission"), 197 | Resources: []string{*resource.ID}, 198 | Policies: []string{*policy.ID}, 199 | Scopes: []string{*scope.ID}, 200 | }, 201 | ResourceType: String(""), 202 | } 203 | _, res, err := k.Permissions.CreateScopePermission(ctx, realm, clientID, permission) 204 | if err != nil { 205 | t.Errorf("Permissions.CreateScopePermission returned error: %v", err) 206 | } 207 | 208 | if res.StatusCode != http.StatusCreated { 209 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 210 | } 211 | 212 | permissions, res, err := k.Permissions.List(ctx, realm, clientID) 213 | if err != nil { 214 | t.Errorf("Permissions.List returned error: %v", err) 215 | } 216 | 217 | if res.StatusCode != http.StatusOK { 218 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 219 | } 220 | 221 | if len(permissions) != 2 { 222 | t.Errorf("got: %d, want: %d", len(permissions), 2) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // User representation. 10 | // 11 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java 12 | type User struct { 13 | ID *string `json:"id,omitempty"` 14 | CreatedTimestamp *int64 `json:"createdTimestamp,omitempty"` 15 | Username *string `json:"username,omitempty"` 16 | Enabled *bool `json:"enabled,omitempty"` 17 | Totp *bool `json:"totp,omitempty"` 18 | EmailVerified *bool `json:"emailVerified,omitempty"` 19 | FirstName *string `json:"firstName,omitempty"` 20 | LastName *string `json:"lastName,omitempty"` 21 | Email *string `json:"email,omitempty"` 22 | DisableableCredentialTypes []string `json:"disableableCredentialTypes,omitempty"` 23 | RequiredActions []string `json:"requiredActions,omitempty"` 24 | NotBefore *int `json:"notBefore,omitempty"` 25 | Access *map[string]bool `json:"access,omitempty"` 26 | Attributes *map[string][]string `json:"attributes,omitempty"` 27 | } 28 | 29 | // Credential representation. 30 | // 31 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java 32 | type Credential struct { 33 | Type *string `json:"type,omitempty"` 34 | Value *string `json:"value,omitempty"` 35 | Temporary *bool `json:"temporary,omitempty"` 36 | } 37 | 38 | // UsersService ... 39 | type UsersService service 40 | 41 | // Create a new user. 42 | func (s *UsersService) Create(ctx context.Context, realm string, user *User) (*http.Response, error) { 43 | u := fmt.Sprintf("admin/realms/%s/users", realm) 44 | req, err := s.keycloak.NewRequest(http.MethodPost, u, user) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return s.keycloak.Do(ctx, req, nil) 50 | } 51 | 52 | // List users. 53 | func (s *UsersService) List(ctx context.Context, realm string) ([]*User, *http.Response, error) { 54 | u := fmt.Sprintf("admin/realms/%s/users", realm) 55 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | var users []*User 61 | res, err := s.keycloak.Do(ctx, req, &users) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | return users, res, nil 67 | } 68 | 69 | // GetByID get a single user by ID. 70 | func (s *UsersService) GetByID(ctx context.Context, realm, id string) (*User, *http.Response, error) { 71 | u := fmt.Sprintf("admin/realms/%s/users/%s", realm, id) 72 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | var user *User 78 | res, err := s.keycloak.Do(ctx, req, &user) 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | 83 | return user, res, nil 84 | } 85 | 86 | // GetByUsername get a single user by username. 87 | func (s *UsersService) GetByUsername(ctx context.Context, realm, username string) ([]*User, *http.Response, error) { 88 | u := fmt.Sprintf("admin/realms/%s/users?username=%s", realm, username) 89 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | 94 | var users []*User 95 | res, err := s.keycloak.Do(ctx, req, &users) 96 | if err != nil { 97 | return nil, nil, err 98 | } 99 | 100 | return users, res, nil 101 | } 102 | 103 | // Update update a single user. 104 | func (s *UsersService) Update(ctx context.Context, realm string, user *User) (*http.Response, error) { 105 | u := fmt.Sprintf("admin/realms/%s/users/%s", realm, *user.ID) 106 | req, err := s.keycloak.NewRequest(http.MethodPut, u, user) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return s.keycloak.Do(ctx, req, nil) 112 | } 113 | 114 | // Delete user. 115 | func (s *UsersService) Delete(ctx context.Context, realm, userID string) (*http.Response, error) { 116 | u := fmt.Sprintf("admin/realms/%s/users/%s", realm, userID) 117 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return s.keycloak.Do(ctx, req, nil) 123 | } 124 | 125 | // ResetPassword sets or resets the user's password. 126 | func (s *UsersService) ResetPassword(ctx context.Context, realm, userID string, credential *Credential) (*http.Response, error) { 127 | u := fmt.Sprintf("admin/realms/%s/users/%s/reset-password", realm, userID) 128 | req, err := s.keycloak.NewRequest(http.MethodPut, u, credential) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return s.keycloak.Do(ctx, req, nil) 134 | } 135 | 136 | // Update user. 137 | 138 | // JoinGroup adds user to a group. 139 | func (s *UsersService) JoinGroup(ctx context.Context, realm, userID, groupID string) (*http.Response, error) { 140 | u := fmt.Sprintf("admin/realms/%s/users/%s/groups/%s", realm, userID, groupID) 141 | req, err := s.keycloak.NewRequest(http.MethodPut, u, nil) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | return s.keycloak.Do(ctx, req, nil) 147 | } 148 | 149 | // LeaveGroup removes a user from a group. 150 | func (s *UsersService) LeaveGroup(ctx context.Context, realm, userID, groupID string) (*http.Response, error) { 151 | u := fmt.Sprintf("admin/realms/%s/users/%s/groups/%s", realm, userID, groupID) 152 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return s.keycloak.Do(ctx, req, nil) 158 | } 159 | 160 | // AddRealmRoles adds realm roles to user. 161 | func (s *UsersService) AddRealmRoles(ctx context.Context, realm, userID string, roles []*Role) (*http.Response, error) { 162 | u := fmt.Sprintf("admin/realms/%s/users/%s/role-mappings/realm", realm, userID) 163 | req, err := s.keycloak.NewRequest(http.MethodPost, u, roles) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | return s.keycloak.Do(ctx, req, nil) 169 | } 170 | 171 | // RemoveRealmRoles removes assigned realm roles from user. 172 | func (s *UsersService) RemoveRealmRoles(ctx context.Context, realm, userID string, roles []*Role) (*http.Response, error) { 173 | u := fmt.Sprintf("admin/realms/%s/users/%s/role-mappings/realm", realm, userID) 174 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, roles) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | return s.keycloak.Do(ctx, req, nil) 180 | } 181 | 182 | // ListRealmRoles returns a list of realm roles assigned to user. 183 | func (s *UsersService) ListRealmRoles(ctx context.Context, realm, userID string) ([]*Role, *http.Response, error) { 184 | u := fmt.Sprintf("admin/realms/%s/users/%s/role-mappings/realm", realm, userID) 185 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 186 | if err != nil { 187 | return nil, nil, err 188 | } 189 | 190 | var roles []*Role 191 | res, err := s.keycloak.Do(ctx, req, &roles) 192 | if err != nil { 193 | return nil, nil, err 194 | } 195 | 196 | return roles, res, nil 197 | } 198 | 199 | // AddClientRoles adds client roles to user. 200 | func (s *UsersService) AddClientRoles(ctx context.Context, realm, userID, clientID string, roles []*Role) (*http.Response, error) { 201 | u := fmt.Sprintf("admin/realms/%s/users/%s/role-mappings/clients/%s", realm, userID, clientID) 202 | req, err := s.keycloak.NewRequest(http.MethodPost, u, roles) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | return s.keycloak.Do(ctx, req, nil) 208 | } 209 | 210 | // RemoveClientRoles removes assigned client roles from user. 211 | func (s *UsersService) RemoveClientRoles(ctx context.Context, realm, userID, clientID string, roles []*Role) (*http.Response, error) { 212 | u := fmt.Sprintf("admin/realms/%s/users/%s/role-mappings/clients/%s", realm, userID, clientID) 213 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, roles) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | return s.keycloak.Do(ctx, req, nil) 219 | } 220 | 221 | // VerifyEmailOptions ... 222 | type VerifyEmailOptions struct { 223 | ClientID string `url:"client_id,omitempty"` 224 | RedirectUri string `url:"redirect_uri,omitempty"` 225 | } 226 | 227 | // Send an email-verification email to the user. 228 | // An email contains a link the user can click to verify their email address. 229 | func (s *UsersService) SendVerifyEmail(ctx context.Context, realm, userID string, opts *VerifyEmailOptions) (*http.Response, error) { 230 | u := fmt.Sprintf("admin/realms/%s/users/%s/send-verify-email", realm, userID) 231 | u, err := addOptions(u, opts) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | req, err := s.keycloak.NewRequest(http.MethodPut, u, nil) 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | return s.keycloak.Do(ctx, req, nil) 242 | } 243 | 244 | // ExecuteActionsEmailOptions ... 245 | type ExecuteActionsEmailOptions struct { 246 | ClientID string `url:"client_id,omitempty"` 247 | Lifespan int `url:"lifespan,omitempty"` 248 | RedirectUri string `url:"redirect_uri,omitempty"` 249 | } 250 | 251 | // ExecuteActionsEmail sends an update account email to the user. 252 | // An email contains a link the user can click to perform a set of required actions. 253 | func (s *UsersService) ExecuteActionsEmail(ctx context.Context, realm, userID string, opts *ExecuteActionsEmailOptions, actions []string) (*http.Response, error) { 254 | u := fmt.Sprintf("admin/realms/%s/users/%s/execute-actions-email", realm, userID) 255 | u, err := addOptions(u, opts) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | req, err := s.keycloak.NewRequest(http.MethodPut, u, actions) 261 | if err != nil { 262 | return nil, err 263 | } 264 | 265 | return s.keycloak.Do(ctx, req, nil) 266 | } 267 | -------------------------------------------------------------------------------- /example_full_test.go: -------------------------------------------------------------------------------- 1 | package keycloak_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/zemirco/keycloak/v2" 11 | "golang.org/x/oauth2" 12 | "golang.org/x/oauth2/clientcredentials" 13 | ) 14 | 15 | const ( 16 | realm = "gorealm" 17 | clientID = "goclient" 18 | ) 19 | 20 | func Example_full() { 21 | // create a new config for the "admin-cli" client 22 | config := oauth2.Config{ 23 | ClientID: "admin-cli", 24 | Endpoint: oauth2.Endpoint{ 25 | TokenURL: "http://localhost:8080/realms/master/protocol/openid-connect/token", 26 | }, 27 | Scopes: []string{"openid"}, 28 | } 29 | 30 | ctx := context.Background() 31 | 32 | // get a token 33 | token, err := config.PasswordCredentialsToken(ctx, "admin", "admin") 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // use the token on every http request 39 | httpClient := config.Client(ctx, token) 40 | 41 | // create a new keycloak client instance 42 | kc, err := keycloak.NewKeycloak(httpClient, "http://localhost:8080/") 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // create a new realm 48 | r := &keycloak.Realm{ 49 | Enabled: keycloak.Bool(true), 50 | ID: keycloak.String(realm), 51 | Realm: keycloak.String(realm), 52 | } 53 | 54 | if _, err := kc.Realms.Create(ctx, r); err != nil { 55 | panic(err) 56 | } 57 | 58 | // clean up 59 | // remove this or comment out to keep the realm and play around in the keycloak gui 60 | defer func() { 61 | if _, err := kc.Realms.Delete(ctx, realm); err != nil { 62 | panic(err) 63 | } 64 | }() 65 | 66 | // create client 67 | client := &keycloak.Client{ 68 | Enabled: keycloak.Bool(true), 69 | ClientID: keycloak.String(clientID), 70 | Protocol: keycloak.String("openid-connect"), 71 | } 72 | 73 | res, err := kc.Clients.Create(ctx, realm, client) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | parts := strings.Split(res.Header.Get("Location"), "/") 79 | id := parts[len(parts)-1] 80 | 81 | // get the client to have all properties 82 | client, res, err = kc.Clients.Get(ctx, realm, id) 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | // update client and enable authorization services 88 | client.RedirectUris = []string{"http://localhost:4200/*"} 89 | client.AuthorizationServicesEnabled = keycloak.Bool(true) 90 | client.ServiceAccountsEnabled = keycloak.Bool(true) 91 | client.PublicClient = keycloak.Bool(false) 92 | 93 | res, err = kc.Clients.Update(ctx, realm, client) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | // create user 99 | userA := &keycloak.User{ 100 | Enabled: keycloak.Bool(true), 101 | Username: keycloak.String("user_a"), 102 | } 103 | 104 | res, err = kc.Users.Create(ctx, realm, userA) 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | parts = strings.Split(res.Header.Get("Location"), "/") 110 | userA.ID = &parts[len(parts)-1] 111 | 112 | // set password for user_a 113 | res, err = kc.Users.ResetPassword(ctx, realm, *userA.ID, &keycloak.Credential{ 114 | Type: keycloak.String("password"), 115 | Value: keycloak.String("mypassword"), 116 | Temporary: keycloak.Bool(false), 117 | }) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | // create user 123 | userB := &keycloak.User{ 124 | Enabled: keycloak.Bool(true), 125 | Username: keycloak.String("user_b"), 126 | } 127 | 128 | res, err = kc.Users.Create(ctx, realm, userB) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | parts = strings.Split(res.Header.Get("Location"), "/") 134 | userB.ID = &parts[len(parts)-1] 135 | 136 | // set password for user_b 137 | res, err = kc.Users.ResetPassword(ctx, realm, *userB.ID, &keycloak.Credential{ 138 | Type: keycloak.String("password"), 139 | Value: keycloak.String("mypassword"), 140 | Temporary: keycloak.Bool(false), 141 | }) 142 | if err != nil { 143 | panic(err) 144 | } 145 | 146 | // create scope 147 | scopeRead := &keycloak.Scope{ 148 | Name: keycloak.String("scope_read"), 149 | } 150 | 151 | scopeRead, res, err = kc.Scopes.Create(ctx, realm, *client.ID, scopeRead) 152 | if err != nil { 153 | panic(err) 154 | } 155 | 156 | // create scope 157 | scopeWrite := &keycloak.Scope{ 158 | Name: keycloak.String("scope_write"), 159 | } 160 | 161 | scopeWrite, res, err = kc.Scopes.Create(ctx, realm, *client.ID, scopeWrite) 162 | if err != nil { 163 | panic(err) 164 | } 165 | 166 | // create first resource 167 | resourceProject1 := &keycloak.Resource{ 168 | Name: keycloak.String("Project_1"), 169 | Uris: []string{"/projects/1"}, 170 | Scopes: []*keycloak.Scope{ 171 | { 172 | ID: keycloak.String(*scopeRead.ID), 173 | Name: keycloak.String("scope_read"), 174 | }, 175 | { 176 | ID: keycloak.String(*scopeWrite.ID), 177 | Name: keycloak.String("scope_write"), 178 | }}, 179 | } 180 | 181 | resourceProject1, res, err = kc.Resources.Create(ctx, realm, *client.ID, resourceProject1) 182 | if err != nil { 183 | panic(err) 184 | } 185 | 186 | // create second resource 187 | resourceProject2 := &keycloak.Resource{ 188 | Name: keycloak.String("Project_2"), 189 | Uris: []string{"/projects/2"}, 190 | Scopes: []*keycloak.Scope{ 191 | { 192 | ID: keycloak.String(*scopeRead.ID), 193 | Name: keycloak.String("scope_read"), 194 | }, 195 | { 196 | ID: keycloak.String(*scopeWrite.ID), 197 | Name: keycloak.String("scope_write"), 198 | }}, 199 | } 200 | 201 | resourceProject2, res, err = kc.Resources.Create(ctx, realm, *client.ID, resourceProject2) 202 | if err != nil { 203 | panic(err) 204 | } 205 | 206 | // create user policy with user_a 207 | userAPolicy := &keycloak.UserPolicy{ 208 | Policy: keycloak.Policy{ 209 | Type: keycloak.String("user"), 210 | Logic: keycloak.String(keycloak.LogicPositive), 211 | DecisionStrategy: keycloak.String(keycloak.DecisionStrategyUnanimous), 212 | Name: keycloak.String("user_a_policy"), 213 | }, 214 | Users: []string{*userA.ID}, 215 | } 216 | 217 | userAPolicy, res, err = kc.Policies.CreateUserPolicy(ctx, realm, *client.ID, userAPolicy) 218 | if err != nil { 219 | panic(err) 220 | } 221 | 222 | // create user policy with user_b 223 | userBPolicy := &keycloak.UserPolicy{ 224 | Policy: keycloak.Policy{ 225 | Type: keycloak.String("user"), 226 | Logic: keycloak.String(keycloak.LogicPositive), 227 | DecisionStrategy: keycloak.String(keycloak.DecisionStrategyUnanimous), 228 | Name: keycloak.String("user_b_policy"), 229 | }, 230 | Users: []string{*userB.ID}, 231 | } 232 | 233 | userBPolicy, res, err = kc.Policies.CreateUserPolicy(ctx, realm, *client.ID, userBPolicy) 234 | if err != nil { 235 | panic(err) 236 | } 237 | 238 | // create first permission 239 | permission1 := &keycloak.ScopePermission{ 240 | Permission: keycloak.Permission{ 241 | Type: keycloak.String("resource"), 242 | Logic: keycloak.String(keycloak.LogicPositive), 243 | DecisionStrategy: keycloak.String(keycloak.DecisionStrategyUnanimous), 244 | Name: keycloak.String("permission__project_1__user_a_policy"), 245 | Resources: []string{*resourceProject1.ID}, 246 | Policies: []string{*userAPolicy.ID}, 247 | Scopes: []string{*scopeRead.ID}, 248 | }, 249 | ResourceType: keycloak.String(""), 250 | } 251 | permission1, res, err = kc.Permissions.CreateScopePermission(ctx, realm, *client.ID, permission1) 252 | if err != nil { 253 | panic(err) 254 | } 255 | 256 | permission2 := &keycloak.ScopePermission{ 257 | Permission: keycloak.Permission{ 258 | Type: keycloak.String("resource"), 259 | Logic: keycloak.String(keycloak.LogicPositive), 260 | DecisionStrategy: keycloak.String(keycloak.DecisionStrategyUnanimous), 261 | Name: keycloak.String("permission__project_2__user_b_policy"), 262 | Resources: []string{*resourceProject2.ID}, 263 | Policies: []string{*userBPolicy.ID}, 264 | Scopes: []string{*scopeRead.ID}, 265 | }, 266 | ResourceType: keycloak.String(""), 267 | } 268 | permission2, res, err = kc.Permissions.CreateScopePermission(ctx, realm, *client.ID, permission2) 269 | if err != nil { 270 | panic(err) 271 | } 272 | 273 | // get client secret 274 | credential, _, err := kc.Clients.GetSecret(ctx, realm, *client.ID) 275 | if err != nil { 276 | panic(err) 277 | } 278 | 279 | // pretend to be user_a 280 | userAConfig := oauth2.Config{ 281 | ClientID: clientID, 282 | ClientSecret: *credential.Value, 283 | Endpoint: oauth2.Endpoint{ 284 | TokenURL: fmt.Sprintf("http://localhost:8080/realms/%s/protocol/openid-connect/token", realm), 285 | }, 286 | } 287 | 288 | userAToken, err := userAConfig.PasswordCredentialsToken(ctx, "user_a", "mypassword") 289 | if err != nil { 290 | panic(err) 291 | } 292 | 293 | clientConfig := clientcredentials.Config{ 294 | ClientID: clientID, 295 | ClientSecret: *credential.Value, 296 | TokenURL: fmt.Sprintf("http://localhost:8080/realms/%s/protocol/openid-connect/token", realm), 297 | EndpointParams: url.Values{ 298 | "grant_type": {"urn:ietf:params:oauth:grant-type:uma-ticket"}, 299 | "permission": {*resourceProject1.Name + "#scope_read"}, 300 | "audience": {clientID}, 301 | }, 302 | } 303 | 304 | ctx = context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{ 305 | Transport: &oauth2.Transport{ 306 | Source: oauth2.StaticTokenSource(userAToken), 307 | }, 308 | }) 309 | 310 | // ask keycloak for permission for reading Project_1 311 | token, err = clientConfig.Token(ctx) 312 | if err != nil { 313 | panic(err) 314 | } 315 | 316 | // ask keycloak for permission for reading Project_2 317 | clientConfig.EndpointParams.Set("permission", *resourceProject2.Name+"#scope_read") 318 | 319 | token, err = clientConfig.Token(ctx) 320 | if err == nil { 321 | panic("got no error, expected one") 322 | } 323 | retrieveError, ok := err.(*oauth2.RetrieveError) 324 | if !ok { 325 | panic(fmt.Errorf("got %T error, expected *RetrieveError; error was: %v", err, err)) 326 | } 327 | 328 | fmt.Println(retrieveError.Response.Status) 329 | // Output: 403 Forbidden 330 | } 331 | -------------------------------------------------------------------------------- /users_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | // create a new user. 12 | func createUser(t *testing.T, k *Keycloak, realm string, username string) string { 13 | t.Helper() 14 | 15 | user := &User{ 16 | Enabled: Bool(true), 17 | Username: String(username), 18 | Email: String(username + "@email.com"), 19 | FirstName: String("first"), 20 | LastName: String("last"), 21 | } 22 | 23 | res, err := k.Users.Create(context.Background(), realm, user) 24 | if err != nil { 25 | t.Errorf("Users.Create returned error: %v", err) 26 | } 27 | 28 | parts := strings.Split(res.Header.Get("Location"), "/") 29 | userID := parts[len(parts)-1] 30 | return userID 31 | } 32 | 33 | func TestUsersService_Create(t *testing.T) { 34 | k := client(t) 35 | 36 | realm := "first" 37 | createRealm(t, k, realm) 38 | 39 | ctx := context.Background() 40 | 41 | user := &User{ 42 | Enabled: Bool(true), 43 | Username: String("username"), 44 | Email: String("user@email.com"), 45 | FirstName: String("first"), 46 | LastName: String("last"), 47 | Attributes: &map[string][]string{ 48 | "some_key": {"some_value"}, 49 | }, 50 | } 51 | 52 | res, err := k.Users.Create(ctx, realm, user) 53 | if err != nil { 54 | t.Errorf("Users.Create returned error: %v", err) 55 | } 56 | 57 | if res.StatusCode != http.StatusCreated { 58 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusCreated) 59 | } 60 | } 61 | 62 | func TestUsersService_GetByID(t *testing.T) { 63 | k := client(t) 64 | 65 | realm := "first" 66 | createRealm(t, k, realm) 67 | id := createUser(t, k, realm, "newuser") 68 | 69 | ctx := context.Background() 70 | user, res, err := k.Users.GetByID(ctx, realm, id) 71 | if err != nil { 72 | t.Errorf("Users.GetByID returned error: %v", err) 73 | } 74 | 75 | if res.StatusCode != http.StatusOK { 76 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 77 | } 78 | if *user.Username != "newuser" { 79 | t.Errorf("got: %s, want: %s", *user.Username, "newuser") 80 | } 81 | } 82 | 83 | func TestUsersService_Update(t *testing.T) { 84 | k := client(t) 85 | 86 | realm := "first" 87 | createRealm(t, k, realm) 88 | id := createUser(t, k, realm, "newuser") 89 | 90 | ctx := context.Background() 91 | user, res, err := k.Users.GetByID(ctx, realm, id) 92 | if err != nil { 93 | t.Errorf("Users.GetByID returned error: %v", err) 94 | } 95 | 96 | if res.StatusCode != http.StatusOK { 97 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 98 | } 99 | 100 | newAttributes := &map[string][]string{ 101 | "some_key": {"other_value"}, 102 | "new_key": {"some_value"}, 103 | } 104 | user.Attributes = newAttributes 105 | 106 | res, err = k.Users.Update(ctx, realm, user) 107 | if err != nil { 108 | t.Errorf("Users.Update returned error: %v", err) 109 | } 110 | 111 | if res.StatusCode != http.StatusNoContent { 112 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 113 | } 114 | 115 | user, res, err = k.Users.GetByID(ctx, realm, id) 116 | if err != nil { 117 | t.Errorf("Users.GetByID returned error: %v", err) 118 | } 119 | 120 | if res.StatusCode != http.StatusOK { 121 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 122 | } 123 | 124 | if !reflect.DeepEqual(user.Attributes, newAttributes) { 125 | t.Errorf("Users.Update attributes do not match: %v != %v", user.Attributes, newAttributes) 126 | } 127 | } 128 | 129 | func TestUsersService_Delete(t *testing.T) { 130 | k := client(t) 131 | 132 | realm := "first" 133 | createRealm(t, k, realm) 134 | 135 | userID := createUser(t, k, realm, "john") 136 | 137 | res, err := k.Users.Delete(context.Background(), realm, userID) 138 | if err != nil { 139 | t.Errorf("Users.Delete returned error: %v", err) 140 | } 141 | 142 | if res.StatusCode != http.StatusNoContent { 143 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 144 | } 145 | } 146 | 147 | func TestUsersService_List(t *testing.T) { 148 | k := client(t) 149 | 150 | realm := "first" 151 | createRealm(t, k, realm) 152 | 153 | createUser(t, k, realm, "john") 154 | createUser(t, k, realm, "mark") 155 | 156 | users, res, err := k.Users.List(context.Background(), realm) 157 | if err != nil { 158 | t.Errorf("Users.List returned error: %v", err) 159 | } 160 | 161 | if res.StatusCode != http.StatusOK { 162 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 163 | } 164 | 165 | // it includes the master realm 166 | if len(users) != 2 { 167 | t.Errorf("got: %d, want: %d", len(users), 2) 168 | } 169 | } 170 | 171 | func TestUsersService_GetByUsername(t *testing.T) { 172 | k := client(t) 173 | 174 | realm := "first" 175 | createRealm(t, k, realm) 176 | 177 | createUser(t, k, realm, "john") 178 | 179 | users, res, err := k.Users.GetByUsername(context.Background(), realm, "john") 180 | if err != nil { 181 | t.Errorf("Users.GetByUsername returned error: %v", err) 182 | } 183 | 184 | if res.StatusCode != http.StatusOK { 185 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 186 | } 187 | 188 | // it includes the master realm 189 | if len(users) != 1 { 190 | t.Errorf("got: %d, want: %d", len(users), 1) 191 | } 192 | 193 | if *users[0].Username != "john" { 194 | t.Errorf("got: %s, want: %s", *users[0].Username, "john") 195 | } 196 | } 197 | 198 | func TestUsersService_ResetPassword(t *testing.T) { 199 | k := client(t) 200 | 201 | realm := "first" 202 | createRealm(t, k, realm) 203 | 204 | userID := createUser(t, k, realm, "john") 205 | 206 | ctx := context.Background() 207 | 208 | credential := &Credential{ 209 | Type: String("password"), 210 | Value: String("mypassword"), 211 | Temporary: Bool(false), 212 | } 213 | res, err := k.Users.ResetPassword(ctx, realm, userID, credential) 214 | if err != nil { 215 | t.Errorf("Users.ResetPassword returned error: %v", err) 216 | } 217 | 218 | if res.StatusCode != http.StatusNoContent { 219 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 220 | } 221 | } 222 | 223 | func TestUsersService_JoinGroup(t *testing.T) { 224 | k := client(t) 225 | 226 | realm := "first" 227 | createRealm(t, k, realm) 228 | 229 | userID := createUser(t, k, realm, "john") 230 | groupID := createGroup(t, k, realm, "group") 231 | 232 | ctx := context.Background() 233 | 234 | res, err := k.Users.JoinGroup(ctx, realm, userID, groupID) 235 | if err != nil { 236 | t.Errorf("Users.JoinGroup returned error: %v", err) 237 | } 238 | 239 | if res.StatusCode != http.StatusNoContent { 240 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 241 | } 242 | } 243 | 244 | func TestUsersService_LeaveGroup(t *testing.T) { 245 | k := client(t) 246 | 247 | realm := "first" 248 | createRealm(t, k, realm) 249 | 250 | userID := createUser(t, k, realm, "john") 251 | groupID := createGroup(t, k, realm, "group") 252 | 253 | ctx := context.Background() 254 | 255 | res, err := k.Users.JoinGroup(ctx, realm, userID, groupID) 256 | if err != nil { 257 | t.Errorf("Users.JoinGroup returned error: %v", err) 258 | } 259 | 260 | if res.StatusCode != http.StatusNoContent { 261 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 262 | } 263 | 264 | res, err = k.Users.LeaveGroup(ctx, realm, userID, groupID) 265 | if err != nil { 266 | t.Errorf("Users.LeaveGroup returned error: %v", err) 267 | } 268 | 269 | if res.StatusCode != http.StatusNoContent { 270 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 271 | } 272 | } 273 | 274 | func TestUsersService_AddRealmRoles(t *testing.T) { 275 | k := client(t) 276 | 277 | realm := "first" 278 | roleName := "role" 279 | 280 | createRealm(t, k, realm) 281 | createRealmRole(t, k, realm, roleName) 282 | userID := createUser(t, k, realm, "user") 283 | 284 | ctx := context.Background() 285 | 286 | // get role in order to assign it 287 | role, _, err := k.RealmRoles.GetByName(ctx, realm, roleName) 288 | if err != nil { 289 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 290 | } 291 | 292 | roles := []*Role{role} 293 | 294 | res, err := k.Users.AddRealmRoles(ctx, realm, userID, roles) 295 | if err != nil { 296 | t.Errorf("Users.AddRealmRoles returned error: %v", err) 297 | } 298 | 299 | if res.StatusCode != http.StatusNoContent { 300 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 301 | } 302 | } 303 | 304 | func TestUsersService_RemoveRealmRoles(t *testing.T) { 305 | k := client(t) 306 | 307 | realm := "first" 308 | roleName := "role" 309 | 310 | createRealm(t, k, realm) 311 | createRealmRole(t, k, realm, roleName) 312 | userID := createUser(t, k, realm, "user") 313 | 314 | ctx := context.Background() 315 | 316 | // get role in order to assign it 317 | role, _, err := k.RealmRoles.GetByName(ctx, realm, roleName) 318 | if err != nil { 319 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 320 | } 321 | 322 | roles := []*Role{role} 323 | 324 | if _, err := k.Users.AddRealmRoles(ctx, realm, userID, roles); err != nil { 325 | t.Errorf("Users.AddRealmRoles returned error: %v", err) 326 | } 327 | 328 | res, err := k.Users.RemoveRealmRoles(ctx, realm, userID, roles) 329 | if err != nil { 330 | t.Errorf("Users.RemoveRealmRoles returned error: %v", err) 331 | } 332 | 333 | if res.StatusCode != http.StatusNoContent { 334 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 335 | } 336 | } 337 | 338 | func TestUsersService_ListRealmRoles(t *testing.T) { 339 | k := client(t) 340 | 341 | realm := "first" 342 | roleName := "role" 343 | 344 | createRealm(t, k, realm) 345 | createRealmRole(t, k, realm, roleName) 346 | userID := createUser(t, k, realm, "user") 347 | 348 | ctx := context.Background() 349 | 350 | // get role in order to assign it 351 | role, _, err := k.RealmRoles.GetByName(ctx, realm, roleName) 352 | if err != nil { 353 | t.Errorf("RealmRoles.GetByName returned error: %v", err) 354 | } 355 | 356 | roles := []*Role{role} 357 | 358 | if _, err := k.Users.AddRealmRoles(ctx, realm, userID, roles); err != nil { 359 | t.Errorf("Users.AddRealmRoles returned error: %v", err) 360 | } 361 | 362 | roles, res, err := k.Users.ListRealmRoles(ctx, realm, userID) 363 | if err != nil { 364 | t.Errorf("Users.ListRealmRoles returned error: %v", err) 365 | } 366 | 367 | if res.StatusCode != http.StatusOK { 368 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusOK) 369 | } 370 | 371 | // "role" and "default-roles-first" 372 | if len(roles) != 2 { 373 | t.Errorf("got: %d, want: %d", len(roles), 2) 374 | } 375 | } 376 | 377 | func TestUsersService_AddClientRoles(t *testing.T) { 378 | k := client(t) 379 | 380 | realm := "first" 381 | 382 | createRealm(t, k, realm) 383 | clientID := createClient(t, k, realm, "client") 384 | createClientRole(t, k, realm, clientID, "role") 385 | userID := createUser(t, k, realm, "user") 386 | 387 | ctx := context.Background() 388 | 389 | // get role in order to assign it 390 | role, _, err := k.ClientRoles.Get(ctx, realm, clientID, "role") 391 | if err != nil { 392 | t.Errorf("ClientRoles.Get returned error: %v", err) 393 | } 394 | 395 | roles := []*Role{role} 396 | 397 | res, err := k.Users.AddClientRoles(ctx, realm, userID, clientID, roles) 398 | if err != nil { 399 | t.Errorf("Users.AddClientRoles returned error: %v", err) 400 | } 401 | 402 | if res.StatusCode != http.StatusNoContent { 403 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 404 | } 405 | } 406 | 407 | func TestUsersService_RemoveClientRoles(t *testing.T) { 408 | 409 | } 410 | 411 | func TestUsersService_SendVerifyEmail(t *testing.T) { 412 | k := client(t) 413 | 414 | realm := "first" 415 | 416 | createRealm(t, k, realm) 417 | userID := createUser(t, k, realm, "user") 418 | 419 | res, err := k.Users.SendVerifyEmail(context.Background(), realm, userID, nil) 420 | if err != nil { 421 | t.Errorf("Users.SendVerifyEmail returned error: %v", err) 422 | } 423 | 424 | if res.StatusCode != http.StatusNoContent { 425 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 426 | } 427 | } 428 | 429 | func TestUsersService_ExecuteActionsEmail(t *testing.T) { 430 | k := client(t) 431 | 432 | realm := "first" 433 | 434 | createRealm(t, k, realm) 435 | userID := createUser(t, k, realm, "user") 436 | 437 | opts := &ExecuteActionsEmailOptions{ 438 | Lifespan: 1000, 439 | } 440 | 441 | res, err := k.Users.ExecuteActionsEmail(context.Background(), realm, userID, opts, []string{"UPDATE_PROFILE"}) 442 | if err != nil { 443 | t.Errorf("Users.ExecuteActionsEmail returned error: %v", err) 444 | } 445 | 446 | if res.StatusCode != http.StatusNoContent { 447 | t.Errorf("got: %d, want: %d", res.StatusCode, http.StatusNoContent) 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /realms.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // IdentityProvider representation. 10 | // 11 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java 12 | type IdentityProvider struct { 13 | } 14 | 15 | // IdentityProviderMapper representation. 16 | // 17 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/IdentityProviderMapperRepresentation.java 18 | type IdentityProviderMapper struct { 19 | } 20 | 21 | // Realm representation. 22 | // 23 | // https://github.com/keycloak/keycloak/blob/master/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java 24 | type Realm struct { 25 | ID *string `json:"id,omitempty"` 26 | Realm *string `json:"realm,omitempty"` 27 | DisplayName *string `json:"displayName,omitempty"` 28 | DisplayNameHTML *string `json:"displayNameHtml,omitempty"` 29 | NotBefore *int `json:"notBefore,omitempty"` 30 | RevokeRefreshToken *bool `json:"revokeRefreshToken,omitempty"` 31 | RefreshTokenMaxReuse *int `json:"refreshTokenMaxReuse,omitempty"` 32 | AccessTokenLifespan *int `json:"accessTokenLifespan,omitempty"` 33 | AccessTokenLifespanForImplicitFlow *int `json:"accessTokenLifespanForImplicitFlow,omitempty"` 34 | SsoSessionIdleTimeout *int `json:"ssoSessionIdleTimeout,omitempty"` 35 | SsoSessionMaxLifespan *int `json:"ssoSessionMaxLifespan,omitempty"` 36 | SsoSessionIdleTimeoutRememberMe *int `json:"ssoSessionIdleTimeoutRememberMe,omitempty"` 37 | SsoSessionMaxLifespanRememberMe *int `json:"ssoSessionMaxLifespanRememberMe,omitempty"` 38 | OfflineSessionIdleTimeout *int `json:"offlineSessionIdleTimeout,omitempty"` 39 | OfflineSessionMaxLifespanEnabled *bool `json:"offlineSessionMaxLifespanEnabled,omitempty"` 40 | OfflineSessionMaxLifespan *int `json:"offlineSessionMaxLifespan,omitempty"` 41 | ClientSessionIdleTimeout *int `json:"clientSessionIdleTimeout,omitempty"` 42 | ClientSessionMaxLifespan *int `json:"clientSessionMaxLifespan,omitempty"` 43 | ClientOfflineSessionIdleTimeout *int `json:"clientOfflineSessionIdleTimeout,omitempty"` 44 | ClientOfflineSessionMaxLifespan *int `json:"clientOfflineSessionMaxLifespan,omitempty"` 45 | AccessCodeLifespan *int `json:"accessCodeLifespan,omitempty"` 46 | AccessCodeLifespanUserAction *int `json:"accessCodeLifespanUserAction,omitempty"` 47 | AccessCodeLifespanLogin *int `json:"accessCodeLifespanLogin,omitempty"` 48 | ActionTokenGeneratedByAdminLifespan *int `json:"actionTokenGeneratedByAdminLifespan,omitempty"` 49 | ActionTokenGeneratedByUserLifespan *int `json:"actionTokenGeneratedByUserLifespan,omitempty"` 50 | Enabled *bool `json:"enabled,omitempty"` 51 | SslRequired *string `json:"sslRequired,omitempty"` 52 | RegistrationAllowed *bool `json:"registrationAllowed,omitempty"` 53 | RegistrationEmailAsUsername *bool `json:"registrationEmailAsUsername,omitempty"` 54 | RememberMe *bool `json:"rememberMe,omitempty"` 55 | VerifyEmail *bool `json:"verifyEmail,omitempty"` 56 | LoginWithEmailAllowed *bool `json:"loginWithEmailAllowed,omitempty"` 57 | DuplicateEmailsAllowed *bool `json:"duplicateEmailsAllowed,omitempty"` 58 | ResetPasswordAllowed *bool `json:"resetPasswordAllowed,omitempty"` 59 | EditUsernameAllowed *bool `json:"editUsernameAllowed,omitempty"` 60 | BruteForceProtected *bool `json:"bruteForceProtected,omitempty"` 61 | PermanentLockout *bool `json:"permanentLockout,omitempty"` 62 | MaxFailureWaitSeconds *int `json:"maxFailureWaitSeconds,omitempty"` 63 | MinimumQuickLoginWaitSeconds *int `json:"minimumQuickLoginWaitSeconds,omitempty"` 64 | WaitIncrementSeconds *int `json:"waitIncrementSeconds,omitempty"` 65 | QuickLoginCheckMilliSeconds *int `json:"quickLoginCheckMilliSeconds,omitempty"` 66 | MaxDeltaTimeSeconds *int `json:"maxDeltaTimeSeconds,omitempty"` 67 | FailureFactor *int `json:"failureFactor,omitempty"` 68 | DefaultRoles []string `json:"defaultRoles,omitempty"` 69 | RequiredCredentials []string `json:"requiredCredentials,omitempty"` 70 | OtpPolicyType *string `json:"otpPolicyType,omitempty"` 71 | OtpPolicyAlgorithm *string `json:"otpPolicyAlgorithm,omitempty"` 72 | OtpPolicyInitialCounter *int `json:"otpPolicyInitialCounter,omitempty"` 73 | OtpPolicyDigits *int `json:"otpPolicyDigits,omitempty"` 74 | OtpPolicyLookAheadWindow *int `json:"otpPolicyLookAheadWindow,omitempty"` 75 | OtpPolicyPeriod *int `json:"otpPolicyPeriod,omitempty"` 76 | OtpSupportedApplications []string `json:"otpSupportedApplications,omitempty"` 77 | WebAuthnPolicyRpEntityName *string `json:"webAuthnPolicyRpEntityName,omitempty"` 78 | WebAuthnPolicySignatureAlgorithms []string `json:"webAuthnPolicySignatureAlgorithms,omitempty"` 79 | WebAuthnPolicyRpID *string `json:"webAuthnPolicyRpId,omitempty"` 80 | WebAuthnPolicyAttestationConveyancePreference *string `json:"webAuthnPolicyAttestationConveyancePreference,omitempty"` 81 | WebAuthnPolicyAuthenticatorAttachment *string `json:"webAuthnPolicyAuthenticatorAttachment,omitempty"` 82 | WebAuthnPolicyRequireResidentKey *string `json:"webAuthnPolicyRequireResidentKey,omitempty"` 83 | WebAuthnPolicyUserVerificationRequirement *string `json:"webAuthnPolicyUserVerificationRequirement,omitempty"` 84 | WebAuthnPolicyCreateTimeout *int `json:"webAuthnPolicyCreateTimeout,omitempty"` 85 | WebAuthnPolicyAvoidSameAuthenticatorRegister *bool `json:"webAuthnPolicyAvoidSameAuthenticatorRegister,omitempty"` 86 | WebAuthnPolicyAcceptableAaguids []string `json:"webAuthnPolicyAcceptableAaguids,omitempty"` 87 | WebAuthnPolicyPasswordlessRpEntityName *string `json:"webAuthnPolicyPasswordlessRpEntityName,omitempty"` 88 | WebAuthnPolicyPasswordlessSignatureAlgorithms []string `json:"webAuthnPolicyPasswordlessSignatureAlgorithms,omitempty"` 89 | WebAuthnPolicyPasswordlessRpID *string `json:"webAuthnPolicyPasswordlessRpId,omitempty"` 90 | WebAuthnPolicyPasswordlessAttestationConveyancePreference *string `json:"webAuthnPolicyPasswordlessAttestationConveyancePreference,omitempty"` 91 | WebAuthnPolicyPasswordlessAuthenticatorAttachment *string `json:"webAuthnPolicyPasswordlessAuthenticatorAttachment,omitempty"` 92 | WebAuthnPolicyPasswordlessRequireResidentKey *string `json:"webAuthnPolicyPasswordlessRequireResidentKey,omitempty"` 93 | WebAuthnPolicyPasswordlessUserVerificationRequirement *string `json:"webAuthnPolicyPasswordlessUserVerificationRequirement,omitempty"` 94 | WebAuthnPolicyPasswordlessCreateTimeout *int `json:"webAuthnPolicyPasswordlessCreateTimeout,omitempty"` 95 | WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister *bool `json:"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister,omitempty"` 96 | WebAuthnPolicyPasswordlessAcceptableAaguids []string `json:"webAuthnPolicyPasswordlessAcceptableAaguids,omitempty"` 97 | BrowserSecurityHeaders *map[string]string `json:"browserSecurityHeaders,omitempty"` 98 | SMTPServer *map[string]string `json:"smtpServer,omitempty"` 99 | EventsEnabled *bool `json:"eventsEnabled,omitempty"` 100 | EventsListeners []string `json:"eventsListeners,omitempty"` 101 | EnabledEventTypes []string `json:"enabledEventTypes,omitempty"` 102 | AdminEventsEnabled *bool `json:"adminEventsEnabled,omitempty"` 103 | AdminEventsDetailsEnabled *bool `json:"adminEventsDetailsEnabled,omitempty"` 104 | IdentityProviders []*IdentityProvider `json:"identityProviders,omitempty"` 105 | IdentityProviderMappers []*IdentityProviderMapper `json:"identityProviderMappers,omitempty"` 106 | InternationalizationEnabled *bool `json:"internationalizationEnabled,omitempty"` 107 | SupportedLocales []string `json:"supportedLocales,omitempty"` 108 | BrowserFlow *string `json:"browserFlow,omitempty"` 109 | RegistrationFlow *string `json:"registrationFlow,omitempty"` 110 | DirectGrantFlow *string `json:"directGrantFlow,omitempty"` 111 | ResetCredentialsFlow *string `json:"resetCredentialsFlow,omitempty"` 112 | ClientAuthenticationFlow *string `json:"clientAuthenticationFlow,omitempty"` 113 | DockerAuthenticationFlow *string `json:"dockerAuthenticationFlow,omitempty"` 114 | Attributes *map[string]string `json:"attributes,omitempty"` 115 | UserManagedAccessAllowed *bool `json:"userManagedAccessAllowed,omitempty"` 116 | } 117 | 118 | // Configuration represents a UMA configuration. 119 | type Configuration struct { 120 | Issuer *string `json:"issuer,omitempty"` 121 | AuthorizationEndpoint *string `json:"authorization_endpoint,omitempty"` 122 | TokenEndpoint *string `json:"token_endpoint,omitempty"` 123 | IntrospectionEndpoint *string `json:"introspection_endpoint,omitempty"` 124 | EndSessionEndpoint *string `json:"end_session_endpoint,omitempty"` 125 | JwksURI *string `json:"jwks_uri,omitempty"` 126 | GrantTypesSupported []string `json:"grant_types_supported,omitempty"` 127 | ResponseTypesSupported []string `json:"response_types_supported,omitempty"` 128 | ResponseModesSupported []string `json:"response_modes_supported,omitempty"` 129 | RegistrationEndpoint *string `json:"registration_endpoint,omitempty"` 130 | TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` 131 | TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` 132 | ScopesSupported []string `json:"scopes_supported,omitempty"` 133 | ResourceRegistrationEndpoint *string `json:"resource_registration_endpoint,omitempty"` 134 | PermissionEndpoint *string `json:"permission_endpoint,omitempty"` 135 | PolicyEndpoint *string `json:"policy_endpoint,omitempty"` 136 | } 137 | 138 | // RealmsService ... 139 | type RealmsService service 140 | 141 | // Create a new realm. 142 | func (s *RealmsService) Create(ctx context.Context, realm *Realm) (*http.Response, error) { 143 | req, err := s.keycloak.NewRequest(http.MethodPost, "admin/realms", realm) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | return s.keycloak.Do(ctx, req, nil) 149 | } 150 | 151 | // List all realms. 152 | func (s *RealmsService) List(ctx context.Context) ([]*Realm, *http.Response, error) { 153 | req, err := s.keycloak.NewRequest(http.MethodGet, "admin/realms", nil) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | 158 | var realms []*Realm 159 | res, err := s.keycloak.Do(ctx, req, &realms) 160 | if err != nil { 161 | return nil, nil, err 162 | } 163 | 164 | return realms, res, nil 165 | } 166 | 167 | // Get realm. 168 | func (s *RealmsService) Get(ctx context.Context, name string) (*Realm, *http.Response, error) { 169 | u := fmt.Sprintf("admin/realms/%s", name) 170 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | 175 | var realm Realm 176 | res, err := s.keycloak.Do(ctx, req, &realm) 177 | if err != nil { 178 | return nil, nil, err 179 | } 180 | 181 | return &realm, res, nil 182 | } 183 | 184 | // Delete realm. 185 | func (s *RealmsService) Delete(ctx context.Context, name string) (*http.Response, error) { 186 | u := fmt.Sprintf("admin/realms/%s", name) 187 | req, err := s.keycloak.NewRequest(http.MethodDelete, u, nil) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return s.keycloak.Do(ctx, req, nil) 193 | } 194 | 195 | // GetConfig gets realm configuration. 196 | func (s *RealmsService) GetConfig(ctx context.Context, name string) (*Configuration, *http.Response, error) { 197 | u := fmt.Sprintf("realms/%s/.well-known/uma2-configuration", name) 198 | req, err := s.keycloak.NewRequest(http.MethodGet, u, nil) 199 | if err != nil { 200 | return nil, nil, err 201 | } 202 | 203 | var config Configuration 204 | res, err := s.keycloak.Do(ctx, req, &config) 205 | if err != nil { 206 | return nil, nil, err 207 | } 208 | 209 | return &config, res, nil 210 | } 211 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 37 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 38 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 39 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 40 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 41 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 42 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 44 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 45 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 46 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 47 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 48 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 49 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 50 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 51 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 52 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 53 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 54 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 55 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 56 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 57 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 58 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 59 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 60 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 65 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 66 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 67 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 68 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 69 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 70 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 71 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 72 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 73 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 74 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 75 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 76 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 77 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 78 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 79 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 81 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 85 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 87 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 88 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 89 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 90 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 91 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 92 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 93 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 94 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 95 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 96 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 97 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 98 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 99 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 100 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 101 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 102 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 103 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 104 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 105 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 106 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 107 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 108 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 109 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 110 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 111 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 112 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 114 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 115 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 116 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 117 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 118 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 119 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 120 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 121 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 124 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 125 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 126 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 127 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 128 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 129 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 130 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 131 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 132 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 133 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 134 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 135 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 136 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 137 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 138 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 139 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 140 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 141 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 142 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 143 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 144 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 145 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 146 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 147 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 148 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 149 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 150 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 151 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 152 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 153 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 154 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 155 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 156 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 157 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 159 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 162 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 163 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 164 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 166 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 167 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 168 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 169 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 171 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 172 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 176 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 177 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 178 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 179 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 180 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 181 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 182 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 183 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 184 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 185 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 186 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 187 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 188 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 189 | golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= 190 | golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 191 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 195 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 200 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 225 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 226 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 227 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 228 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 229 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 230 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 231 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 232 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 233 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 235 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 236 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 237 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 239 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 240 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 241 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 242 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 243 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 244 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 245 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 246 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 247 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 248 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 249 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 250 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 251 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 252 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 253 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 254 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 255 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 256 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 257 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 258 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 259 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 260 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 261 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 262 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 263 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 264 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 265 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 266 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 267 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 268 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 269 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 270 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 271 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 272 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 273 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 274 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 276 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 277 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 278 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 279 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 280 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 281 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 282 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 283 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 284 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 285 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 286 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 287 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 288 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 289 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 290 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 291 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 292 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 293 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 294 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 295 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 296 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 297 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 298 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 299 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 300 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 301 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 302 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 303 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 304 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 305 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 306 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 307 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 308 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 309 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 310 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 311 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 312 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 313 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 314 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 315 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 316 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 317 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 318 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 319 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 320 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 321 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 322 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 323 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 324 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 325 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 326 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 327 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 328 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 329 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 330 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 331 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 332 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 333 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 334 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 335 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 336 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 337 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 338 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 339 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 340 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 341 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 342 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 343 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 344 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 345 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 346 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 347 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 348 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 349 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 350 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 351 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 352 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 353 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 354 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 355 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 356 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 357 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 358 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 359 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 360 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 361 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 362 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 363 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 364 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 365 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 366 | --------------------------------------------------------------------------------