├── .gitignore ├── keycloak ├── service.go ├── errors.go ├── auth │ ├── token_test.go │ ├── auth_example_test.go │ ├── token.go │ └── auth.go ├── errors_test.go ├── types_test.go ├── client_test.go ├── types.go ├── realm_service.go ├── client.go ├── user_service_test.go ├── users.go ├── clients.go ├── user_service.go └── realms.go ├── .golangci.yml ├── integration ├── helpers_test.go ├── user_test.go ├── docker-compose.yml ├── suite_test.go └── realm_test.go ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── Gopkg.toml ├── .gitlab-ci.yml ├── LICENSE ├── .circleci └── config.yml ├── Readme.md ├── Makefile └── Gopkg.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /bin 4 | /lint.json 5 | /.testCoverage.txt* -------------------------------------------------------------------------------- /keycloak/service.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | type service struct { 4 | client *Client 5 | } 6 | -------------------------------------------------------------------------------- /keycloak/errors.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import "fmt" 4 | 5 | // Error represents an API error 6 | type Error struct { 7 | Message string 8 | Code int 9 | } 10 | 11 | func (e *Error) Error() string { 12 | return fmt.Sprintf("%s: %d", e.Message, e.Code) 13 | } 14 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - vendor$ 4 | 5 | linters: 6 | enable: 7 | - deadcode 8 | - unconvert 9 | - errcheck 10 | - vet 11 | - vetshadow 12 | - golint 13 | - gosimple 14 | - misspell 15 | 16 | issues: 17 | exclude-rules: 18 | - path: keycloak/service\.go 19 | linters: 20 | - structcheck 21 | -------------------------------------------------------------------------------- /keycloak/auth/token_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "golang.org/x/oauth2" 6 | "testing" 7 | ) 8 | 9 | func TestToken_Oauth2Token(t *testing.T) { 10 | a := assert.New(t) 11 | 12 | tkn := &Token{} 13 | otkn := tkn.Oauth2Token() 14 | 15 | a.Equal(tkn, Extract(otkn)) 16 | } 17 | 18 | func TestToken_Oauth2Token_Empty(t *testing.T) { 19 | a := assert.New(t) 20 | 21 | tkn := &oauth2.Token{} 22 | a.Nil(Extract(tkn)) 23 | } 24 | -------------------------------------------------------------------------------- /integration/helpers_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 10 | 11 | func pseudoRandString() string { 12 | 13 | rand.Seed(time.Now().UnixNano()) 14 | 15 | prefix := make([]byte, 5) 16 | for i := range prefix { 17 | prefix[i] = chars[rand.Intn(len(chars))] 18 | } 19 | 20 | return fmt.Sprintf("%s-%d", string(prefix), time.Now().Unix()) 21 | } 22 | -------------------------------------------------------------------------------- /keycloak/errors_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var errorTests = []struct { 9 | Error Error 10 | ExpectedValue string 11 | }{ 12 | { 13 | Error{}, 14 | ": 0", 15 | }, 16 | { 17 | Error{ 18 | Code: 401, 19 | Message: "Not authorized", 20 | }, 21 | "Not authorized: 401", 22 | }, 23 | } 24 | 25 | func TestError(t *testing.T) { 26 | a := assert.New(t) 27 | 28 | for _, tt := range errorTests { 29 | a.Equal(tt.ExpectedValue, tt.Error.Error()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Environment (please complete the following information):** 21 | - `go version` [e.g. `go version go1.11.1 darwin/amd64`] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /keycloak/types_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var timeMarshalCases = []struct { 11 | Time UnixTime 12 | ExpectedValue []byte 13 | }{ 14 | { 15 | Time: UnixTime(time.Unix(1257894000, 78000000)), 16 | ExpectedValue: []byte("1257894000078"), 17 | }, 18 | { 19 | Time: UnixTime(time.Unix(1530029597, 8000000)), 20 | ExpectedValue: []byte("1530029597008"), 21 | }, 22 | } 23 | 24 | func TestTimeMarshalUnmarshal(t *testing.T) { 25 | a := assert.New(t) 26 | 27 | for _, tt := range timeMarshalCases { 28 | v, err := json.Marshal(tt.Time) 29 | 30 | a.NoError(err) 31 | a.Equal(tt.ExpectedValue, v) 32 | 33 | var v2 UnixTime 34 | 35 | err = json.Unmarshal(v, &v2) 36 | 37 | a.NoError(err) 38 | a.NotNil(v2) 39 | a.Equal(tt.Time.String(), v2.String()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "gopkg.in/resty.v1" 30 | version = "1.7.0" 31 | 32 | [[constraint]] 33 | branch = "v1" 34 | name = "gopkg.in/jarcoal/httpmock.v1" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: circleci/golang:1.11 2 | 3 | variables: 4 | REPO_NAME: github.com/Azuka/keycloak-admin-go 5 | 6 | cache: 7 | paths: 8 | - /apt-cache 9 | - /go/src/github.com 10 | - /go/src/golang.org 11 | - /go/src/google.golang.org 12 | - /go/src/gopkg.in 13 | 14 | # The problem is that to be able to use go get, one needs to put 15 | # the repository in the $GOPATH. So for example if your gitlab domain 16 | # is gitlab.com, and that your repository is namespace/project, and 17 | # the default GOPATH being /go, then you'd need to have your 18 | # repository in /go/src/gitlab.com/namespace/project 19 | # Thus, making a symbolic link corrects this. 20 | before_script: 21 | - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) 22 | - cp -r $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME 23 | - cd $GOPATH/src/$REPO_NAME 24 | - make init-ci 25 | 26 | stages: 27 | - test 28 | 29 | unit: 30 | stage: test 31 | script: 32 | - cd $GOPATH/src/$REPO_NAME 33 | - make test-ci -------------------------------------------------------------------------------- /keycloak/client_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func ExampleNewClient() { 14 | u, _ := url.Parse("http://localhost/auth/admin") 15 | c := NewClient(*u, http.DefaultClient) 16 | userID, _ := c.Users.Create(context.Background(), "myRealm", &UserRepresentation{ 17 | Username: "hello-world", 18 | }) 19 | fmt.Println("UserID: ", userID) 20 | } 21 | 22 | func TestNewClient(t *testing.T) { 23 | a := assert.New(t) 24 | 25 | url, _ := url.Parse("http://localhost/keycloak/auth/admin/") 26 | 27 | client := NewClient(*url, http.DefaultClient) 28 | 29 | a.NotNil(client) 30 | a.NotNil(client) 31 | a.NotNil(client.Users) 32 | a.False(client.restClient.Debug) 33 | } 34 | 35 | func TestNewClientDebug(t *testing.T) { 36 | a := assert.New(t) 37 | 38 | client := NewClient(url.URL{}, http.DefaultClient) 39 | client.Debug() 40 | a.True(client.restClient.Debug) 41 | } 42 | -------------------------------------------------------------------------------- /integration/user_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "github.com/Azuka/keycloak-admin-go/keycloak" 5 | ) 6 | 7 | func (suite *integrationTester) TestUserFetch() { 8 | users, err := suite.client.Users.Find(suite.ctx, keycloakAdminRealm, map[string]string{ 9 | "username": keycloakAdmin, 10 | }) 11 | suite.NotNil(users, suite.version) 12 | suite.NoError(err, suite.version) 13 | suite.Len(users, 1, suite.version) 14 | suite.Equal(keycloakAdmin, users[0].Username, suite.version) 15 | suite.True(*users[0].Enabled, suite.version) 16 | 17 | user := users[0] 18 | t := true 19 | user.EmailVerified = &t 20 | 21 | err = suite.client.Users.Update(suite.ctx, keycloakAdminRealm, &user) 22 | suite.NoError(err, suite.version) 23 | } 24 | 25 | func (suite *integrationTester) TestUserCreate() { 26 | 27 | user := &keycloak.UserRepresentation{ 28 | Username: pseudoRandString(), 29 | Email: pseudoRandString() + "@example.com", 30 | } 31 | 32 | id, err := suite.client.Users.Create(suite.ctx, keycloakAdminRealm, user) 33 | 34 | suite.NotEmpty(id, suite.version) 35 | suite.NoError(err, suite.version) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Azuka Okuleye 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. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | references: 5 | # Common configuration for all jobs 6 | defaults: &defaults 7 | docker: 8 | - image: circleci/golang:1.9 9 | working_directory: /go/src/github.com/Azuka/keycloak-admin-go 10 | steps: 11 | - checkout 12 | - run: 13 | name: Install dependencies 14 | command: | 15 | make init-ci 16 | - run: 17 | name: Lint and run tests 18 | command: | 19 | PATH="$(pwd)/bin:$PATH" make lint 20 | make test-circle 21 | - store_test_results: 22 | path: /tmp/test-results 23 | 24 | version: 2 25 | jobs: 26 | go-1.10: 27 | <<: *defaults 28 | docker: 29 | - image: circleci/golang:1.10 30 | go-1.11: 31 | <<: *defaults 32 | docker: 33 | - image: circleci/golang:1.11 34 | go-1.12: 35 | <<: *defaults 36 | docker: 37 | - image: circleci/golang:1.12 38 | 39 | workflows: 40 | version: 2 41 | test-all-go-versions: 42 | jobs: 43 | - go-1.10 44 | - go-1.11 45 | - go-1.12 46 | -------------------------------------------------------------------------------- /keycloak/auth/auth_example_test.go: -------------------------------------------------------------------------------- 1 | package auth_test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Azuka/keycloak-admin-go/keycloak/auth" 7 | ) 8 | 9 | func ExampleConfig_Client() { 10 | config := auth.Config{ 11 | ClientID: "admin-cli", 12 | TokenURL: "https://keycloak.local/auth/realms/master/protocol/openid-connect/token", 13 | Username: "keycloak", 14 | Password: "password", 15 | GrantType: auth.PasswordGrant, 16 | } 17 | 18 | client := config.Client(context.Background()) 19 | 20 | // This will make an authenticated request 21 | _, _ = client.Get("https://keycloak.local/auth/admin/realms/master/users?username=keycloak-admin") 22 | } 23 | 24 | func ExampleConfig_Client_client_credentials() { 25 | config := auth.Config{ 26 | ClientID: "admin-cli", 27 | TokenURL: "https://keycloak.local/auth/realms/master/protocol/openid-connect/token", 28 | ClientSecret: "my-secret", 29 | GrantType: auth.ClientCredentialsGrant, 30 | } 31 | 32 | client := config.Client(context.Background()) 33 | 34 | // This will make an authenticated request 35 | _, _ = client.Get("https://keycloak.local/auth/admin/realms/master/users?username=keycloak-admin") 36 | } 37 | -------------------------------------------------------------------------------- /integration/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | keycloak-db: 5 | image: postgres:9.6 6 | volumes: 7 | - ../build/database:/var/lib/postgresql/data 8 | - ../build/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/databases.sh 9 | environment: 10 | POSTGRES_USER: keycloak 11 | POSTGRES_PASSWORD: changeme 12 | POSTGRES_MULTIPLE_DATABASES: '"keycloak-4.0.0","keycloak-4.1.0","keycloak-4.8.0"' 13 | 14 | keycloak-4.0.0: 15 | image: jboss/keycloak:4.0.0.Final 16 | restart: always 17 | ports: 18 | - 9090:8080 19 | depends_on: 20 | - keycloak-db 21 | environment: 22 | KEYCLOAK_USER: keycloak-admin 23 | KEYCLOAK_PASSWORD: changeme 24 | DB_DATABASE: keycloak-4.0.0 25 | DB_USER: keycloak 26 | DB_PASSWORD: changeme 27 | DB_ADDR: keycloak-db 28 | DB_VENDOR: postgres 29 | 30 | keycloak-4.8.0: 31 | image: jboss/keycloak:4.8.0.Final 32 | restart: always 33 | ports: 34 | - 9098:8080 35 | depends_on: 36 | - keycloak-db 37 | environment: 38 | KEYCLOAK_USER: keycloak-admin 39 | KEYCLOAK_PASSWORD: changeme 40 | DB_DATABASE: keycloak-4.8.0 41 | DB_USER: keycloak 42 | DB_PASSWORD: changeme 43 | DB_ADDR: keycloak-db 44 | DB_VENDOR: postgres 45 | -------------------------------------------------------------------------------- /keycloak/types.go: -------------------------------------------------------------------------------- 1 | //go:generate gomodifytags -file $GOFILE -struct MultivaluedHashMap -add-options json=omitempty -add-tags json -w -transform camelcase 2 | 3 | package keycloak 4 | 5 | import ( 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // AttributeMap represents a map of attributes 12 | type AttributeMap map[string]interface{} 13 | 14 | // UnixTime is an alias for a date time from Keycloak 15 | // which comes in as an int32 16 | type UnixTime time.Time 17 | 18 | // MarshalJSON lets UnixTime implement the json.Marshaler interface 19 | func (t UnixTime) MarshalJSON() ([]byte, error) { 20 | return []byte(strconv.FormatInt(time.Time(t).UnixNano()/int64(time.Millisecond), 10)), nil 21 | } 22 | 23 | // UnmarshalJSON lets UnixTime implement the json.Unmarshaler interface 24 | func (t *UnixTime) UnmarshalJSON(s []byte) error { 25 | r := strings.Replace(string(s), `"`, ``, -1) 26 | 27 | q, err := strconv.ParseInt(r, 10, 64) 28 | if err != nil { 29 | return err 30 | } 31 | *(*time.Time)(t) = time.Unix(0, q*int64(time.Millisecond)) 32 | return nil 33 | } 34 | 35 | func (t UnixTime) String() string { 36 | return time.Time(t).String() 37 | } 38 | 39 | // MultivaluedHashMap multivalued map 40 | // easyjson:json 41 | type MultivaluedHashMap struct { 42 | Empty bool `json:"empty,omitempty"` 43 | LoadFactor float64 `json:"loadFactor,omitempty"` 44 | Threshold int32 `json:"threshold,omitempty"` 45 | } 46 | -------------------------------------------------------------------------------- /keycloak/realm_service.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import "context" 4 | 5 | // RealmService interacts with all realm resources 6 | type RealmService service 7 | 8 | // NewRealmService returns a new user service for working with user resources 9 | // in a realm. 10 | func NewRealmService(c *Client) *RealmService { 11 | return &RealmService{ 12 | client: c, 13 | } 14 | } 15 | 16 | // Get realm with realm name (not id!) 17 | func (rs *RealmService) Get(ctx context.Context, realm string) (*RealmRepresentation, error) { 18 | 19 | // nolint: goconst 20 | path := "/realms/{realm}" 21 | 22 | rr := &RealmRepresentation{} 23 | 24 | _, err := rs.client.newRequest(ctx). 25 | SetPathParams(map[string]string{ 26 | "realm": realm, 27 | }). 28 | SetResult(rr). 29 | Get(path) 30 | 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return rr, nil 36 | 37 | } 38 | 39 | // Create realm with realm, known in Keycloak as import 40 | func (rs *RealmService) Create(ctx context.Context, realm *RealmRepresentation) error { 41 | path := "/realms" 42 | _, err := rs.client.newRequest(ctx). 43 | SetBody(realm). 44 | Post(path) 45 | 46 | return err 47 | } 48 | 49 | // Clear a realm's user cache 50 | func (rs *RealmService) ClearUserCache(ctx context.Context, realm string) error { 51 | path := "/realms/{realm}/clear-user-cache" 52 | _, err := rs.client.newRequest(ctx). 53 | SetPathParams(map[string]string{ 54 | "realm": realm, 55 | }). 56 | Post(path) 57 | 58 | return err 59 | } 60 | 61 | // Clear a realm's cache 62 | func (rs *RealmService) ClearCache(ctx context.Context, realm string) error { 63 | path := "/realms/{realm}/clear-realm-cache" 64 | _, err := rs.client.newRequest(ctx). 65 | SetPathParams(map[string]string{ 66 | "realm": realm, 67 | }). 68 | Post(path) 69 | 70 | return err 71 | } 72 | 73 | // Delete realm with realm name (not id!) 74 | func (rs *RealmService) Delete(ctx context.Context, realm string) error { 75 | 76 | // nolint: goconst 77 | path := "/realms/{realm}" 78 | 79 | _, err := rs.client.newRequest(ctx). 80 | SetPathParams(map[string]string{ 81 | "realm": realm, 82 | }). 83 | SetResult(realm). 84 | Delete(path) 85 | 86 | return err 87 | } 88 | -------------------------------------------------------------------------------- /keycloak/client.go: -------------------------------------------------------------------------------- 1 | // Package keycloak contains a client and relevant data structs for interacting 2 | // with the Keycloak Admin REST API 3 | // 4 | // For mapping, see https://www.keycloak.org/docs-api/4.0/rest-api/index.html 5 | package keycloak 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | 11 | "context" 12 | "fmt" 13 | 14 | "gopkg.in/resty.v1" 15 | ) 16 | 17 | const userAgent = "go/keycloak-admin" 18 | 19 | // Client is the API client for talking to keycloak admin 20 | type Client struct { 21 | BaseURL url.URL 22 | restClient *resty.Client 23 | 24 | // Services for working with various keycloak resources 25 | Users *UserService 26 | Realm *RealmService 27 | } 28 | 29 | // NewClient creates a new client instance set to talk to the keycloak service 30 | // as well as the various services for working with specific resources 31 | func NewClient(u url.URL, c *http.Client) *Client { 32 | 33 | restClient := resty.NewWithClient(c) 34 | 35 | client := &Client{ 36 | BaseURL: u, 37 | restClient: restClient, 38 | } 39 | 40 | client.Users = NewUserService(client) 41 | client.Realm = NewRealmService(client) 42 | 43 | return client 44 | } 45 | 46 | // Debug enables debugging for requests 47 | func (c *Client) Debug() { 48 | c.restClient.SetDebug(true) 49 | } 50 | 51 | // newRequest creates a new request 52 | func (c *Client) newRequest(ctx context.Context) *resty.Request { 53 | 54 | if c.restClient == nil { 55 | c.restClient = resty.NewWithClient(http.DefaultClient) 56 | } 57 | 58 | return c.restClient. 59 | // Set base url per request 60 | SetHostURL(c.BaseURL.String()). 61 | // Set redirect policy based on host name 62 | SetRedirectPolicy(resty.DomainCheckRedirectPolicy(c.BaseURL.Hostname())). 63 | // Setup error handling for non <= 399 codes 64 | OnAfterResponse(handleResponse). 65 | R(). 66 | SetContext(ctx). 67 | SetHeader("UserAgent", userAgent) 68 | } 69 | 70 | // handleResponse handles 400+ http error codes 71 | func handleResponse(i *resty.Client, response *resty.Response) error { 72 | if response.StatusCode() < 400 { 73 | return nil 74 | } 75 | 76 | return &Error{ 77 | Message: fmt.Sprintf("%s %s: %s", response.Request.Method, response.Request.URL, response.Status()), 78 | Code: response.StatusCode(), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # keycloak-admin-go 2 | 3 | [![](https://godoc.org/github.com/Azuka/keycloak-admin-go/keycloak?status.svg)](http://godoc.org/github.com/Azuka/keycloak-admin-go/keycloak) 4 | [![pipeline status](https://gitlab.com/Azuka/keycloak-admin-go/badges/master/pipeline.svg)](https://gitlab.com/Azuka/keycloak-admin-go/commits/master) 5 | [![coverage report](https://gitlab.com/Azuka/keycloak-admin-go/badges/master/coverage.svg)](https://gitlab.com/Azuka/keycloak-admin-go/commits/master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/Azuka/keycloak-admin-go)](https://goreportcard.com/report/github.com/Azuka/keycloak-admin-go) 7 | [![CircleCI](https://circleci.com/gh/Azuka/keycloak-admin-go.svg?style=svg)](https://circleci.com/gh/Azuka/keycloak-admin-go) 8 | 9 | Keycloak admin client in go. 10 | 11 | This is still highly unstable as more of the admin api endpoints and parameters are added. 12 | 13 | ## Usage 14 | ```shell 15 | go get -u github.com/Azuka/keycloak-admin-go/... 16 | ``` 17 | 18 | ## Local Development 19 | ```shell 20 | make init 21 | make test 22 | make integration 23 | #optionally 24 | make integration-clean 25 | ``` 26 | ### Local CI 27 | - Install CircleCI locally: https://circleci.com/docs/2.0/local-cli 28 | 29 | ## Wish List 30 | - [x] Add authentication integration tests 31 | - [ ] Attack Detection 32 | - [ ] Authentication Management 33 | - [ ] Client Attribute Certificate 34 | - [ ] Client Initial Access 35 | - [ ] Client Registration Policy 36 | - [ ] Client Role Mappings 37 | - [ ] Client Scopes 38 | - [ ] Clients 39 | - [ ] Component 40 | - [ ] Groups 41 | - [ ] Identity Providers 42 | - [ ] Key 43 | - [ ] Protocol Mappers 44 | - [ ] Realms Admin 45 | - [x] Get realm 46 | - [ ] Import realm 47 | - [ ] Update realm 48 | - [x] Delete realm 49 | - [ ] Get admin events 50 | - [ ] Delete admin events 51 | - [ ] Role Mapper 52 | - [ ] Roles 53 | - [ ] Roles (by ID) 54 | - [ ] Scope Mappings 55 | - [ ] User Storage Provider 56 | - [ ] Users 57 | - [x] Get user 58 | - [x] Search users 59 | - [x] Create user 60 | - [x] Update user 61 | - [x] Profile information 62 | - [x] Groups 63 | - [x] Sessions, Consents 64 | - [ ] Root 65 | 66 | ## Thanks to 67 | - https://gopkg.in/resty.v1: quick and dirty REST client 68 | - https://godoc.org/golang.org/x/oauth2: for the shamelessly copied authentication 69 | - https://github.com/fatih/gomodifytags: because I'm too lazy to type json struct tags 70 | -------------------------------------------------------------------------------- /integration/suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | "time" 10 | 11 | "github.com/Azuka/keycloak-admin-go/keycloak" 12 | "github.com/Azuka/keycloak-admin-go/keycloak/auth" 13 | "github.com/cenkalti/backoff" 14 | "github.com/stretchr/testify/suite" 15 | ) 16 | 17 | const keycloakAdmin = "keycloak-admin" 18 | const keycloakPassword = "changeme" 19 | const keycloakAdminRealm = "master" 20 | const keycloakAdminClientID = "admin-cli" 21 | 22 | var keyCloakEndpoints = map[string]string{ 23 | "4.0.0": "http://127.0.0.1:9090/auth/", 24 | "4.8.0": "http://127.0.0.1:9098/auth/", 25 | } 26 | 27 | type integrationTester struct { 28 | ready chan struct{} 29 | suite.Suite 30 | client *keycloak.Client 31 | ctx context.Context 32 | version string 33 | endpoint string 34 | } 35 | 36 | func (suite *integrationTester) httpClient() *http.Client { 37 | config := auth.Config{ 38 | ClientID: keycloakAdminClientID, 39 | Username: keycloakAdmin, 40 | Password: keycloakPassword, 41 | GrantType: auth.PasswordGrant, 42 | TokenURL: suite.endpoint + "realms/" + keycloakAdminRealm + "/protocol/openid-connect/token", 43 | } 44 | 45 | http.DefaultClient.Timeout = time.Second * 5 46 | return config.Client(suite.ctx) 47 | } 48 | 49 | func (suite *integrationTester) SetupSuite() { 50 | suite.ready = make(chan struct{}) 51 | suite.ctx = context.Background() 52 | 53 | connect := func() error { 54 | _, err := http.Get(suite.endpoint) 55 | 56 | if err == nil { 57 | close(suite.ready) 58 | return nil 59 | } 60 | 61 | fmt.Println("Waiting to connect to keycloak: ", err) 62 | 63 | return err 64 | } 65 | 66 | // Setup test client 67 | u, _ := url.Parse(suite.endpoint + "admin") 68 | suite.client = keycloak.NewClient(*u, suite.httpClient()) 69 | suite.client.Debug() 70 | 71 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) 72 | defer cancel() 73 | 74 | go func() { 75 | err := backoff.Retry(connect, backoff.WithContext(backoff.NewExponentialBackOff(), ctx)) 76 | if err != nil { 77 | panic(fmt.Errorf("error connecting: %+v", err)) 78 | } 79 | }() 80 | 81 | <-suite.ready 82 | } 83 | 84 | func TestKeycloakAdminIntegration(t *testing.T) { 85 | for version, endpoint := range keyCloakEndpoints { 86 | suite.Run(t, &integrationTester{ 87 | version: version, 88 | endpoint: endpoint, 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /keycloak/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "golang.org/x/oauth2" 5 | "time" 6 | ) 7 | 8 | // raw value for storing keycloak token in oauth2 token 9 | const keycloakTokenKey = "keycloakTokenKey" 10 | 11 | // TokenSource builds on the existing oauth.TokenSource 12 | // with an additional method for fetching a raw keycloak token 13 | type TokenSource interface { 14 | oauth2.TokenSource 15 | 16 | // KeycloakToken returns a keycloak token 17 | KeycloakToken() (*Token, error) 18 | } 19 | 20 | // Token is the token as received from keycloak 21 | type Token struct { 22 | // AccessToken is the token that authorizes and authenticates 23 | // the requests. 24 | AccessToken string `json:"access_token"` 25 | 26 | // TokenType is the type of token. 27 | // The Type method returns either this or "Bearer", the default. 28 | TokenType string `json:"token_type,omitempty"` 29 | 30 | // RefreshToken is a token that's used by the application 31 | // (as opposed to the user) to refresh the access token 32 | // if it expires. 33 | RefreshToken string `json:"refresh_token,omitempty"` 34 | 35 | // Expiry is the optional expiration time of the access token. 36 | // 37 | // If zero, TokenSource implementations will reuse the same 38 | // token forever and RefreshToken or equivalent 39 | // mechanisms for that TokenSource will not be used. 40 | Expiry time.Time `json:"expiry,omitempty"` 41 | 42 | // ExpiresIn is the time this token is valid for, per Keycloak 43 | ExpiresIn int64 `json:"expires_in,omitempty"` 44 | 45 | // RefreshExpiresIn is the time the refresh token expires 46 | RefreshExpiresIn int64 `json:"refresh_expires_in,omitempty"` 47 | 48 | // NotBeforePolicy is likely the Keycloak clock skew 49 | NotBeforePolicy int64 `json:"not_before_policy,,omitempty"` 50 | 51 | // SessionState means something in keycloak 52 | SessionState string `json:"session_state,omitempty"` 53 | 54 | // Scope is the token scope 55 | Scope string `json:"scope,omitempty"` 56 | } 57 | 58 | // Oauth2Token returns an oauth2 token with the underlying original keycloak token 59 | func (t *Token) Oauth2Token() *oauth2.Token { 60 | 61 | tkn := &oauth2.Token{ 62 | AccessToken: t.AccessToken, 63 | TokenType: t.TokenType, 64 | RefreshToken: t.RefreshToken, 65 | Expiry: t.Expiry, 66 | } 67 | 68 | return tkn.WithExtra(map[string]interface{}{ 69 | keycloakTokenKey: t, 70 | }) 71 | } 72 | 73 | // Extract extracts a keycloak token from an oauth one 74 | func Extract(o *oauth2.Token) *Token { 75 | if tkn, ok := o.Extra(keycloakTokenKey).(*Token); ok { 76 | return tkn 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /keycloak/user_service_test.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | "gopkg.in/jarcoal/httpmock.v1" 11 | "gopkg.in/resty.v1" 12 | ) 13 | 14 | func ExampleNewUserService() { 15 | userService := NewUserService(&Client{}) 16 | _, _ = userService.Create(context.TODO(), "my-realm", &UserRepresentation{}) 17 | } 18 | 19 | type userServiceTests struct { 20 | userService *UserService 21 | suite.Suite 22 | } 23 | 24 | func (suite *userServiceTests) SetupSuite() { 25 | c := &Client{ 26 | BaseURL: url.URL{ 27 | Scheme: "https", 28 | Path: "", 29 | Host: "keycloak.local", 30 | }, 31 | restClient: resty.New().OnAfterResponse(handleResponse), 32 | } 33 | c.Debug() 34 | suite.userService = NewUserService(c) 35 | } 36 | 37 | func (suite *userServiceTests) SetupTest() { 38 | httpmock.ActivateNonDefault(suite.userService.client.restClient.GetClient()) 39 | } 40 | 41 | func (suite *userServiceTests) TeardownTest() { 42 | httpmock.DeactivateAndReset() 43 | } 44 | 45 | func TestNewUserService(t *testing.T) { 46 | a := assert.New(t) 47 | c := &Client{} 48 | userService := NewUserService(c) 49 | 50 | a.NotNil(userService) 51 | a.Equal(c, userService.client) 52 | } 53 | 54 | func (suite *userServiceTests) TestUserServiceCreateUser() { 55 | response := httpmock.NewStringResponse(201, "") 56 | response.Header.Add("Location", "https://keycloak.local/realms/my-realm/users/my-awesome-id") 57 | responder := httpmock.ResponderFromResponse(response) 58 | 59 | httpmock.RegisterResponder("POST", "https://keycloak.local/realms/my-realm/users", responder) 60 | 61 | id, err := suite.userService.Create(context.TODO(), "my-realm", &UserRepresentation{ 62 | Username: "me", 63 | }) 64 | suite.NoError(err) 65 | suite.Equal("my-awesome-id", id) 66 | } 67 | 68 | func (suite *userServiceTests) TestUserServiceCreateUserFailure() { 69 | response := httpmock.NewStringResponse(500, "") 70 | responder := httpmock.ResponderFromResponse(response) 71 | 72 | httpmock.RegisterResponder("POST", "https://keycloak.local/realms/my-realm/users", responder) 73 | 74 | _, err := suite.userService.Create(context.TODO(), "my-realm", &UserRepresentation{ 75 | Username: "me", 76 | }) 77 | suite.NotNil(err) 78 | 79 | actualError, ok := err.(*Error) 80 | 81 | suite.True(ok) 82 | suite.NotNil(actualError) 83 | suite.Equal(500, actualError.Code) 84 | } 85 | 86 | func (suite *userServiceTests) TestUserServiceUpdateUser() { 87 | response := httpmock.NewStringResponse(204, "") 88 | response.Header.Add("Location", "https://keycloak.local/realms/my-realm/users/my-awesome-id") 89 | responder := httpmock.ResponderFromResponse(response) 90 | 91 | httpmock.RegisterResponder("PUT", "https://keycloak.local/realms/my-realm/users/abc", responder) 92 | 93 | err := suite.userService.Update(context.TODO(), "my-realm", &UserRepresentation{ 94 | Username: "me", 95 | ID: "abc", 96 | }) 97 | suite.NoError(err) 98 | } 99 | 100 | func TestUserServiceMethods(t *testing.T) { 101 | suite.Run(t, &userServiceTests{}) 102 | } 103 | -------------------------------------------------------------------------------- /integration/realm_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "github.com/Azuka/keycloak-admin-go/keycloak" 5 | ) 6 | 7 | func (suite *integrationTester) TestRealmFetch() { 8 | realm, err := suite.client.Realm.Get(suite.ctx, keycloakAdminRealm) 9 | suite.NotNil(realm, suite.version) 10 | suite.NoError(err, suite.version) 11 | suite.Equal(keycloakAdminRealm, realm.ID, suite.version) 12 | } 13 | 14 | func (suite *integrationTester) TestRealmDelete() { 15 | realmID := pseudoRandString() 16 | realmName := pseudoRandString() 17 | 18 | newRealm := &keycloak.RealmRepresentation{ 19 | ID: realmID, 20 | Realm: realmName, 21 | } 22 | 23 | err := suite.client.Realm.Create(suite.ctx, newRealm) 24 | suite.NoError(err, suite.version) 25 | 26 | err = suite.client.Realm.Delete(suite.ctx, realmName) 27 | suite.NoError(err, suite.version) 28 | } 29 | 30 | func (suite *integrationTester) TestRealmCreate() { 31 | realmID := pseudoRandString() 32 | realmName := pseudoRandString() 33 | t := func() *bool { b := true; return &b }() 34 | newRealm := &keycloak.RealmRepresentation{ 35 | ID: realmID, 36 | Realm: realmName, 37 | AccessCodeLifespan: 1, 38 | AccessCodeLifespanLogin: 2, 39 | AccessCodeLifespanUserAction: 3, 40 | AccessTokenLifespan: 4, 41 | AccessTokenLifespanForImplicitFlow: 5, 42 | AccountTheme: "base", 43 | ActionTokenGeneratedByAdminLifespan: 6, 44 | ActionTokenGeneratedByUserLifespan: 7, 45 | AdminEventsDetailsEnabled: t, 46 | AdminEventsEnabled: t, 47 | AdminTheme: "base", 48 | DisplayName: "realmDisplayName", 49 | DisplayNameHTML: "realmDisplayNameHTML", 50 | } 51 | 52 | err := suite.client.Realm.Create(suite.ctx, newRealm) 53 | suite.NoError(err, suite.version) 54 | 55 | actualRealm, err := suite.client.Realm.Get(suite.ctx, realmName) 56 | suite.NoError(err, suite.version) 57 | suite.NotNil(actualRealm, suite.version) 58 | suite.Equal(actualRealm.ID, newRealm.ID, suite.version) 59 | suite.Equal(actualRealm.Realm, newRealm.Realm, suite.version) 60 | 61 | suite.Equal(actualRealm.AccessCodeLifespan, newRealm.AccessCodeLifespan, suite.version) 62 | suite.Equal(actualRealm.AccessCodeLifespanLogin, newRealm.AccessCodeLifespanLogin, suite.version) 63 | suite.Equal(actualRealm.AccessCodeLifespanUserAction, newRealm.AccessCodeLifespanUserAction, suite.version) 64 | suite.Equal(actualRealm.AccessTokenLifespan, newRealm.AccessTokenLifespan, suite.version) 65 | suite.Equal(actualRealm.AccessTokenLifespanForImplicitFlow, newRealm.AccessTokenLifespanForImplicitFlow, suite.version) 66 | suite.Equal(actualRealm.AccountTheme, newRealm.AccountTheme, suite.version) 67 | suite.Equal(actualRealm.ActionTokenGeneratedByAdminLifespan, newRealm.ActionTokenGeneratedByAdminLifespan, suite.version) 68 | suite.Equal(actualRealm.ActionTokenGeneratedByUserLifespan, newRealm.ActionTokenGeneratedByUserLifespan, suite.version) 69 | suite.Equal(actualRealm.AdminEventsDetailsEnabled, newRealm.AdminEventsDetailsEnabled, suite.version) 70 | suite.Equal(actualRealm.AdminEventsEnabled, newRealm.AdminEventsEnabled, suite.version) 71 | suite.Equal(actualRealm.AdminTheme, newRealm.AdminTheme, suite.version) 72 | suite.Equal(actualRealm.DisplayName, newRealm.DisplayName, suite.version) 73 | suite.Equal(actualRealm.DisplayNameHTML, newRealm.DisplayNameHTML, suite.version) 74 | } 75 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | ## Color definition 3 | ################################################################ 4 | NO_COLOR = \x1b[0m 5 | OK_COLOR = \x1b[32;01m 6 | WARN_COLOR = \x1b[33;01m 7 | ERROR_COLOR = \x1b[31;01m 8 | 9 | ################################################################ 10 | ## Helpers 11 | ################################################################ 12 | PWD := $(shell pwd) 13 | GO_PACKAGES = $(shell go list ./... | grep -v vendor | grep -v integration) 14 | GO_INTEGRATION_PACKAGES = $(shell go list ./... | grep integration) 15 | GO_FILES = $(shell find . -name "*.go" | grep -v vendor | uniq) 16 | CI_TEST_REPORTS ?= /tmp/test-results 17 | 18 | .PHONY: init-ci 19 | init-ci: 20 | @echo "$(OK_COLOR)==> Installing minimal build requirements$(NO_COLOR)" 21 | dep ensure -v 22 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh 23 | go get -u github.com/jstemmer/go-junit-report 24 | 25 | .PHONY: init 26 | init: init-ci 27 | @echo "$(OK_COLOR)==> Installing dev build requirements$(NO_COLOR)" 28 | go get -u github.com/fatih/gomodifytags 29 | go get -u github.com/mailru/easyjson/... 30 | 31 | # Format files 32 | .PHONY: format 33 | format: 34 | @echo "$(OK_COLOR)==> Formatting$(NO_COLOR)" 35 | gofmt -s -l -w $(GO_FILES) 36 | 37 | # Generate files 38 | .PHONY: generate 39 | generate: 40 | @echo "$(OK_COLOR)==> Generating code$(NO_COLOR)" 41 | @go generate ./... 42 | 43 | # Lint 44 | .PHONY: lint 45 | lint: 46 | @echo "$(OK_COLOR)==> Linting$(NO_COLOR)$(NO_COLOR)" 47 | ./bin/golangci-lint run 48 | 49 | # Test 50 | .PHONY: test 51 | test: format lint 52 | @echo "$(OK_COLOR)==> Testing $(NO_COLOR)" 53 | go test -race -cover $(GO_PACKAGES) 54 | 55 | # Test integration 56 | .PHONY: integration-start 57 | integration-start: 58 | docker-compose -f $(PWD)/integration/docker-compose.yml up -d 59 | 60 | .PHONY: integration 61 | integration: integration-start integration-test integration-stop 62 | 63 | .PHONY: integration-stop 64 | integration-stop: 65 | docker-compose -f $(PWD)/integration/docker-compose.yml down 66 | 67 | .PHONY: integration-clean 68 | integration-clean: 69 | rm -rf $(PWD)/build/database 70 | 71 | .PHONY: integration-test 72 | integration-test: 73 | @echo "$(OK_COLOR)==> Integration testing $(NO_COLOR)" 74 | go test -race -v $(GO_INTEGRATION_PACKAGES) 75 | 76 | # Generate coverage 77 | .PHONY: coverage 78 | coverage: 79 | @echo "$(OK_COLOR)==> Generating Coverage Report$(NO_COLOR)" 80 | mkdir -p $(CI_ARTIFACTS)/htmlcov 81 | overalls -project=$(PKG) -covermode=count 82 | go tool cover -html=overalls.coverprofile -o $(COVER_HTML) 83 | 84 | # CI test 85 | .PHONY: test-ci 86 | #code coverage regex for Gitlab: ^total:(\s+)\(statements\)(\s+)(\d+(?:\.\d+)?%) 87 | test-ci: 88 | @echo "$(OK_COLOR)==> Running ci test$(NO_COLOR)" 89 | mkdir -p $(CI_TEST_REPORTS) 90 | /bin/bash -c "set -euxo pipefail; \ 91 | go test -v -short -race -cover -coverprofile .testCoverage.txt $(GO_PACKAGES) | tee >(go-junit-report > $(CI_TEST_REPORTS)/report.xml); \ 92 | sed '/_easyjson.go/d' .testCoverage.txt > .testCoverage.txt.bak; mv .testCoverage.txt.bak .testCoverage.txt; go tool cover -func=.testCoverage.txt" 93 | 94 | # CircleCI test 95 | .PHONY: test-circle 96 | test-circle: 97 | @echo "$(OK_COLOR)==> Running circle test$(NO_COLOR)" 98 | mkdir -p $(CI_TEST_REPORTS) 99 | /bin/bash -c "set -euxo pipefail; \ 100 | go test -v -short -race -cover $(GO_PACKAGES) | tee >(go-junit-report > $(CI_TEST_REPORTS)/report.xml)" 101 | 102 | # CI Lint 103 | .PHONY: lint-ci 104 | lint-ci: 105 | @echo "$(OK_COLOR)==> Running CI lint$(NO_COLOR)" 106 | /bin/golangci-lint run --out-format json > lint.json 107 | 108 | # Quick test for rapid dev-feedback cycles 109 | .PHONY: qt 110 | qt: 111 | @echo "$(OK_COLOR)==> Running quick test$(NO_COLOR)" 112 | go test -short $(GO_PACKAGES) 113 | 114 | .PHONY: local-ci 115 | local-ci: 116 | @echo "$(OK_COLOR)==> Running CI locally. Did you run brew install gitlab-runner and CircleCI?$(NO_COLOR)" 117 | circleci local execute --job go-1.9 118 | circleci local execute --job go-1.10 119 | circleci local execute --job go-1.11 120 | brew services start gitlab-runner 121 | gitlab-runner exec docker unit 122 | gitlab-runner exec docker lint 123 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:2209584c0f7c9b68c23374e659357ab546e1b70eec2761f03280f69a8fd23d77" 6 | name = "github.com/cenkalti/backoff" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e" 10 | version = "v2.0.0" 11 | 12 | [[projects]] 13 | digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" 14 | name = "github.com/davecgh/go-spew" 15 | packages = ["spew"] 16 | pruneopts = "UT" 17 | revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" 18 | version = "v1.1.1" 19 | 20 | [[projects]] 21 | digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d" 22 | name = "github.com/golang/protobuf" 23 | packages = ["proto"] 24 | pruneopts = "UT" 25 | revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" 26 | version = "v1.2.0" 27 | 28 | [[projects]] 29 | digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" 30 | name = "github.com/pmezard/go-difflib" 31 | packages = ["difflib"] 32 | pruneopts = "UT" 33 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 34 | version = "v1.0.0" 35 | 36 | [[projects]] 37 | digest = "1:5110e3d4f130772fd39e6ce8208ad1955b242ccfcc8ad9d158857250579c82f4" 38 | name = "github.com/stretchr/testify" 39 | packages = [ 40 | "assert", 41 | "require", 42 | "suite", 43 | ] 44 | pruneopts = "UT" 45 | revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" 46 | version = "v1.2.2" 47 | 48 | [[projects]] 49 | branch = "master" 50 | digest = "1:ea6ed8fabeb1373bd53b9398248676eb52ec139c788450b316a078b5a06387a0" 51 | name = "golang.org/x/net" 52 | packages = [ 53 | "context", 54 | "context/ctxhttp", 55 | "idna", 56 | "publicsuffix", 57 | ] 58 | pruneopts = "UT" 59 | revision = "ab400d30ebde25116e68880440d31222a016f2a7" 60 | 61 | [[projects]] 62 | branch = "master" 63 | digest = "1:81bafe9ef203750ed218e086d9930b21c086232ccc2a272e104d2af56baf6df5" 64 | name = "golang.org/x/oauth2" 65 | packages = [ 66 | ".", 67 | "internal", 68 | ] 69 | pruneopts = "UT" 70 | revision = "ca4130e427c7982e64cb9b5e717ff67bdb725037" 71 | 72 | [[projects]] 73 | digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" 74 | name = "golang.org/x/text" 75 | packages = [ 76 | "collate", 77 | "collate/build", 78 | "internal/colltab", 79 | "internal/gen", 80 | "internal/tag", 81 | "internal/triegen", 82 | "internal/ucd", 83 | "language", 84 | "secure/bidirule", 85 | "transform", 86 | "unicode/bidi", 87 | "unicode/cldr", 88 | "unicode/norm", 89 | "unicode/rangetable", 90 | ] 91 | pruneopts = "UT" 92 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 93 | version = "v0.3.0" 94 | 95 | [[projects]] 96 | digest = "1:08206298775e5b462e6c0333f4471b44e63f1a70e42952b6ede4ecc9572281eb" 97 | name = "google.golang.org/appengine" 98 | packages = [ 99 | "internal", 100 | "internal/base", 101 | "internal/datastore", 102 | "internal/log", 103 | "internal/remote_api", 104 | "internal/urlfetch", 105 | "urlfetch", 106 | ] 107 | pruneopts = "UT" 108 | revision = "4a4468ece617fc8205e99368fa2200e9d1fad421" 109 | version = "v1.3.0" 110 | 111 | [[projects]] 112 | branch = "v1" 113 | digest = "1:e6a5421ece62f71bef4f9d957747f5837900caa2e5e343da6e124a1b88abbfd0" 114 | name = "gopkg.in/jarcoal/httpmock.v1" 115 | packages = ["."] 116 | pruneopts = "UT" 117 | revision = "c463961d8bfec5823f8100e620bcc73e9afdd7d4" 118 | 119 | [[projects]] 120 | digest = "1:559e4b9d3613025c3b53aff0aa15f41ed6fa82c24cd06e56932b9f0df4ba9ebb" 121 | name = "gopkg.in/resty.v1" 122 | packages = ["."] 123 | pruneopts = "UT" 124 | revision = "92a651cff2687ad3083767fb04b6f53d258b546c" 125 | version = "v1.10.1" 126 | 127 | [solve-meta] 128 | analyzer-name = "dep" 129 | analyzer-version = 1 130 | input-imports = [ 131 | "github.com/cenkalti/backoff", 132 | "github.com/stretchr/testify/assert", 133 | "github.com/stretchr/testify/suite", 134 | "golang.org/x/net/context", 135 | "golang.org/x/net/context/ctxhttp", 136 | "golang.org/x/oauth2", 137 | "gopkg.in/jarcoal/httpmock.v1", 138 | "gopkg.in/resty.v1", 139 | ] 140 | solver-name = "gps-cdcl" 141 | solver-version = 1 142 | -------------------------------------------------------------------------------- /keycloak/auth/auth.go: -------------------------------------------------------------------------------- 1 | // Package auth is copied almost verbatim from golang.org/x/oauth2/clientcredentials 2 | // 3 | // This is because the package above doesn't allow overwriting the grant_type key 4 | // TODO: Clean up and implement/reuse a true keycloak auth 5 | package auth 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "encoding/json" 15 | "io" 16 | "io/ioutil" 17 | 18 | "golang.org/x/net/context" 19 | "golang.org/x/net/context/ctxhttp" 20 | "golang.org/x/oauth2" 21 | ) 22 | 23 | const ( 24 | // ClientCredentialsGrant applies to client credentials 25 | ClientCredentialsGrant = "client_credentials" 26 | 27 | // PasswordGrant is for the password grant 28 | PasswordGrant = "password" 29 | ) 30 | 31 | // skew for token expiry 32 | const expirationSkew = 5 33 | 34 | // Config describes a 2-legged OAuth2 flow, with both the 35 | // client application information and the server's endpoint URLs. 36 | type Config struct { 37 | // ClientID is the application's ID. This should be set for both 38 | // password and client credentials grants 39 | ClientID string 40 | 41 | // ClientSecret is the application's secret. 42 | ClientSecret string 43 | 44 | // Username is the username (if using the password grant). 45 | Username string 46 | 47 | // Password is user's password (if using the password grant). 48 | Password string 49 | 50 | // GrantType is the auth grant type 51 | GrantType string 52 | 53 | // TokenURL is the resource server's token endpoint 54 | // URL. This is a constant specific to each server. 55 | TokenURL string 56 | 57 | // Scope specifies optional requested permissions. 58 | Scopes []string 59 | 60 | // EndpointParams specifies additional parameters for requests to the token endpoint. 61 | EndpointParams url.Values 62 | 63 | HTTPClient *http.Client 64 | } 65 | 66 | // Token uses client credentials to retrieve a token. 67 | // The HTTP client to use is derived from the context. 68 | // If nil, http.DefaultClient is used. 69 | func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { 70 | return c.TokenSource(ctx).Token() 71 | } 72 | 73 | // Client returns an HTTP client using the provided token. 74 | // The token will auto-refresh as necessary. The underlying 75 | // HTTP transport will be obtained using the provided context. 76 | // The returned client and its Transport should not be modified. 77 | func (c *Config) Client(ctx context.Context) *http.Client { 78 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 79 | } 80 | 81 | // TokenSource returns a TokenSource that returns t until t expires, 82 | // automatically refreshing it as necessary using the provided context and the 83 | // client ID and client secret. 84 | // 85 | // Most users will use Config.Client instead. 86 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 87 | source := &tokenSource{ 88 | ctx: ctx, 89 | conf: c, 90 | } 91 | return oauth2.ReuseTokenSource(nil, source) 92 | } 93 | 94 | type tokenSource struct { 95 | ctx context.Context 96 | conf *Config 97 | } 98 | 99 | // KeycloakToken refreshes the token by using a new request. 100 | // tokens received this way do not include a refresh token 101 | func (c *tokenSource) KeycloakToken() (*Token, error) { 102 | v := url.Values{} 103 | 104 | // Set scopes 105 | if len(c.conf.Scopes) > 0 { 106 | v.Set("scope", strings.Join(c.conf.Scopes, " ")) 107 | } 108 | 109 | // Set client_id and client_secret 110 | if c.conf.ClientID != "" { 111 | v.Set("client_id", c.conf.ClientID) 112 | } 113 | if c.conf.ClientSecret != "" { 114 | v.Set("client_secret", c.conf.ClientSecret) 115 | } 116 | 117 | // Set grant type 118 | if c.conf.GrantType != "" { 119 | v.Set("grant_type", c.conf.GrantType) 120 | } 121 | 122 | // Set username and password 123 | if c.conf.Username != "" { 124 | v.Set("username", c.conf.Username) 125 | } 126 | if c.conf.Password != "" { 127 | v.Set("password", c.conf.Password) 128 | } 129 | 130 | for k, p := range c.conf.EndpointParams { 131 | if _, ok := v[k]; ok { 132 | return nil, fmt.Errorf("keycloak oauth2: cannot overwrite parameter %q", k) 133 | } 134 | v[k] = p 135 | } 136 | 137 | req, err := http.NewRequest("POST", c.conf.TokenURL, strings.NewReader(v.Encode())) 138 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | r, err := ctxhttp.Do(c.ctx, c.conf.HTTPClient, req) 144 | 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | if r.Body == nil { 150 | return nil, fmt.Errorf("oauth2: empty keycloak auth response") 151 | } 152 | 153 | // nolint: errcheck 154 | defer r.Body.Close() 155 | 156 | body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) 157 | if err != nil { 158 | return nil, fmt.Errorf("oauth2: cannot fetch keycloak token: %v", err) 159 | } 160 | if code := r.StatusCode; code < 200 || code > 299 { 161 | return nil, fmt.Errorf("oauth2: cannot fetch keycloak token: %v\nResponse: %s", r.Status, body) 162 | } 163 | 164 | tk := &Token{} 165 | 166 | err = json.Unmarshal(body, tk) 167 | 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | tk.Expiry = time.Now().Add(time.Second * time.Duration(tk.ExpiresIn-expirationSkew)) 173 | 174 | return tk, nil 175 | } 176 | 177 | // Token returns the oauth2.Token representation of the keycloak token 178 | func (c *tokenSource) Token() (*oauth2.Token, error) { 179 | 180 | tkn, err := c.KeycloakToken() 181 | 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | return tkn.Oauth2Token(), nil 187 | } 188 | -------------------------------------------------------------------------------- /keycloak/users.go: -------------------------------------------------------------------------------- 1 | //go:generate gomodifytags -file $GOFILE -struct UserRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 2 | //go:generate gomodifytags -file $GOFILE -struct UserConsentRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 3 | //go:generate gomodifytags -file $GOFILE -struct CredentialRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 4 | //go:generate gomodifytags -file $GOFILE -struct FederatedIdentityRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 5 | //go:generate gomodifytags -file $GOFILE -struct UserSessionRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 6 | //go:generate gomodifytags -file $GOFILE -struct GroupRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 7 | 8 | package keycloak 9 | 10 | // UserConsentRepresentation represents client consents 11 | type UserConsentRepresentation struct { 12 | ClientID string `json:"clientId,omitempty"` 13 | CreatedDate *UnixTime `json:"createdDate,omitempty"` 14 | GrantedClientScopes []string `json:"grantedClientScopes,omitempty"` 15 | LastUpdatedDate *UnixTime `json:"lastUpdatedDate,omitempty"` 16 | } 17 | 18 | // CredentialRepresentation represents credentials for a user or client 19 | type CredentialRepresentation struct { 20 | Algorithm string `json:"algorithm,omitempty"` 21 | Counter int32 `json:"counter,omitempty"` 22 | CreatedDate *UnixTime `json:"createdDate,omitempty"` 23 | Device string `json:"device,omitempty"` 24 | Digits int32 `json:"digits,omitempty"` 25 | HashIterations int32 `json:"hashIterations,omitempty"` 26 | HashedSaltedValue string `json:"hashedSaltedValue,omitempty"` 27 | Period int32 `json:"period,omitempty"` 28 | Salt string `json:"salt,omitempty"` 29 | Temporary *bool `json:"temporary,omitempty"` 30 | Type string `json:"type,omitempty"` 31 | Value string `json:"value,omitempty"` 32 | } 33 | 34 | // FederatedIdentityRepresentation represents a federated identity 35 | type FederatedIdentityRepresentation struct { 36 | IdentityProvider string `json:"identityProvider,omitempty"` 37 | UserID string `json:"userId,omitempty"` 38 | UserName string `json:"userName,omitempty"` 39 | } 40 | 41 | // UserRepresentation represents a realm user in Keycloak 42 | type UserRepresentation struct { 43 | Access AttributeMap `json:"access,omitempty"` 44 | Attributes AttributeMap `json:"attributes,omitempty"` 45 | ClientRoles AttributeMap `json:"clientRoles,omitempty"` 46 | ClientConsents []UserConsentRepresentation `json:"clientConsents,omitempty"` 47 | CreatedTimestamp *UnixTime `json:"createdTimestamp,omitempty"` 48 | Credentials []CredentialRepresentation `json:"credentials,omitempty"` 49 | DisableCredentialTypes []string `json:"disableCredentialTypes,omitempty"` 50 | Email string `json:"email,omitempty"` 51 | EmailVerified *bool `json:"emailVerified,omitempty"` 52 | Enabled *bool `json:"enabled,omitempty"` 53 | FederatedIdentities []FederatedIdentityRepresentation `json:"federatedIdentities,omitempty"` 54 | FederationLink string `json:"federationLink,omitempty"` 55 | FirstName string `json:"firstName,omitempty"` 56 | Groups []string `json:"groups,omitempty"` 57 | ID string `json:"id,omitempty"` 58 | LastName string `json:"lastName,omitempty"` 59 | NotBefore *UnixTime `json:"notBefore,omitempty"` 60 | Origin string `json:"origin,omitempty"` 61 | RealmRoles []string `json:"realmRoles,omitempty"` 62 | RequiredActions []string `json:"requiredActions,omitempty"` 63 | Self string `json:"self,omitempty"` 64 | ServiceAccountClientID string `json:"serviceAccountClientId,omitempty"` 65 | Username string `json:"username,omitempty"` 66 | } 67 | 68 | // UserSessionRepresentation is a single session for a user 69 | type UserSessionRepresentation struct { 70 | Clients AttributeMap `json:"clients,omitempty"` 71 | ID string `json:"id,omitempty"` 72 | IPAddress string `json:"ipAddress,omitempty"` 73 | LastAccess *UnixTime `json:"lastAccess,omitempty"` 74 | Start *UnixTime `json:"start,omitempty"` 75 | UserID string `json:"userID,omitempty"` 76 | UserName string `json:"userName,omitempty"` 77 | } 78 | 79 | // GroupRepresentation represents a single user group in a realm 80 | type GroupRepresentation struct { 81 | Access AttributeMap `json:"access,omitempty"` 82 | Attributes AttributeMap `json:"attributes,omitempty"` 83 | ClientRoles AttributeMap `json:"clientRoles,omitempty"` 84 | ID string `json:"id,omitempty"` 85 | Name string `json:"name,omitempty"` 86 | Path string `json:"path,omitempty"` 87 | RealmRoles []string `json:"realmRoles,omitempty"` 88 | SubGroups []GroupRepresentation `json:"subGroups,omitempty"` 89 | } 90 | -------------------------------------------------------------------------------- /keycloak/clients.go: -------------------------------------------------------------------------------- 1 | //go:generate gomodifytags -file $GOFILE -struct ClientRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 2 | //go:generate gomodifytags -file $GOFILE -struct ResourceServerRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 3 | //go:generate gomodifytags -file $GOFILE -struct PolicyRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 4 | //go:generate gomodifytags -file $GOFILE -struct ScopeRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 5 | //go:generate gomodifytags -file $GOFILE -struct ResourceRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 6 | //go:generate gomodifytags -file $GOFILE -struct ProtocolMapperRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 7 | 8 | package keycloak 9 | 10 | const ( 11 | // PolicyEnforcementModeEnforcing marks policy enforcement as enforcing 12 | PolicyEnforcementModeEnforcing = "ENFORCING" 13 | // PolicyEnforcementModePermissive marks policy enforcement as permissive 14 | PolicyEnforcementModePermissive = "PERMISSIVE" 15 | // PolicyEnforcementModeDisabled marks policy enforcement as disabled 16 | PolicyEnforcementModeDisabled = "DISABLED" 17 | 18 | // DecisionstrategyAffirmative sets decision strategy to affirmative 19 | DecisionstrategyAffirmative = "AFFIRMATIVE" 20 | // DecisionstrategyUnanimous sets decision strategy to unanimous 21 | DecisionstrategyUnanimous = "UNANIMOUS" 22 | // DecisionstrategyConsensus sets decision strategy to consensus 23 | DecisionstrategyConsensus = "CONSENSUS" 24 | ) 25 | 26 | // ClientRepresentation represents a client's configuration in a realm 27 | type ClientRepresentation struct { 28 | Access string `json:"access,omitempty"` 29 | AdminURL string `json:"adminUrl,omitempty"` 30 | Attributes AttributeMap `json:"attributes,omitempty"` 31 | AuthenticationFlowBindingOverrides AttributeMap `json:"authenticationFlowBindingOverrides,omitempty"` 32 | AuthorizationServicesEnabled *bool `json:"authorizationServicesEnabled,omitempty"` 33 | AuthorizationSettings *ResourceServerRepresentation `json:"authorizationSettings,omitempty"` 34 | BaseURL string `json:"baseURL,omitempty"` 35 | BearerOnly *bool `json:"bearerOnly,omitempty"` 36 | ClientAuthenticatorType string `json:"clientAuthenticatorType,omitempty"` 37 | ClientID string `json:"clientID,omitempty"` 38 | ConsentRequired *bool `json:"consentRequired,omitempty"` 39 | DefaultClientScopes []string `json:"defaultClientScopes,omitempty"` 40 | DefaultRoles []string `json:"defaultRoles,omitempty"` 41 | Description string `json:"description,omitempty"` 42 | DirectAccessGrantsEnabled *bool `json:"directAccessGrantsEnabled,omitempty"` 43 | Enabled *bool `json:"enabled,omitempty"` 44 | FrontChannelLogout *bool `json:"frontChannelLogout,omitempty"` 45 | FullScopeAllowed *bool `json:"fullScopeAllowed,omitempty"` 46 | ID string `json:"id,omitempty"` 47 | ImplicitFlowEnabled *bool `json:"implicitFlowEnabled,omitempty"` 48 | Name string `json:"name,omitempty"` 49 | NodeRegistrationTimeout *UnixTime `json:"nodeRegistrationTimeout,omitempty"` 50 | NotBefore *UnixTime `json:"notBefore,omitempty"` 51 | OptionalClientScopes []string `json:"optionalClientScopes,omitempty"` 52 | Origin string `json:"origin,omitempty"` 53 | Protocol string `json:"protocol,omitempty"` 54 | ProtocolMappers []ProtocolMapperRepresentation `json:"protocolMappers,omitempty"` 55 | PublicClient *bool `json:"publicClient,omitempty"` 56 | RedirectURIs []string `json:"redirectURIs,omitempty"` 57 | RegisteredNodes AttributeMap `json:"registeredNodes,omitempty"` 58 | RegistrationAccessToken string `json:"registrationAccessToken,omitempty"` 59 | RootURL string `json:"rootURL,omitempty"` 60 | Secret string `json:"secret,omitempty"` 61 | ServiceAccountsEnabled *bool `json:"serviceAccountsEnabled,omitempty"` 62 | StandardFlowEnabled *bool `json:"standardFlowEnabled,omitempty"` 63 | SurrogateAuthRequired *bool `json:"surrogateAuthRequired,omitempty"` 64 | WebOrigins []string `json:"webOrigins,omitempty"` 65 | } 66 | 67 | // ResourceServerRepresentation represents the authorization settings for a realm client 68 | type ResourceServerRepresentation struct { 69 | AllowRemoteResourceManagement *bool `json:"allowRemoteResourceManagement,omitempty"` 70 | ClientID string `json:"clientID,omitempty"` 71 | ID string `json:"id,omitempty"` 72 | Name string `json:"name,omitempty"` 73 | Policies []PolicyRepresentation `json:"policies,omitempty"` 74 | PolicyEnforcementMode string `json:"policyEnforcementMode,omitempty"` 75 | Resources []ResourceRepresentation `json:"resources,omitempty"` 76 | Scopes []ScopeRepresentation `json:"scopes,omitempty"` 77 | } 78 | 79 | // PolicyRepresentation represents the policies attached to the 80 | // resource server for a realm client 81 | type PolicyRepresentation struct { 82 | Config AttributeMap `json:"config,omitempty"` 83 | DecisionStrategy string `json:"decisionStrategy,omitempty"` 84 | Description string `json:"description,omitempty"` 85 | ID string `json:"id,omitempty"` 86 | Logic string `json:"logic,omitempty"` //enum (POSITIVE, NEGATIVE) 87 | Name string `json:"name,omitempty"` 88 | Owner string `json:"owner,omitempty"` 89 | Policies []string `json:"policies,omitempty"` 90 | Resources []string `json:"resources,omitempty"` 91 | Scopes []string `json:"scopes,omitempty"` 92 | Type string `json:"type,omitempty"` 93 | } 94 | 95 | // ScopeRepresentation represents scopes defined for a 96 | // resource server, user, or resource 97 | type ScopeRepresentation struct { 98 | DisplayName string `json:"displayName,omitempty"` 99 | IconURI string `json:"iconURI,omitempty"` 100 | ID string `json:"id,omitempty"` 101 | Name string `json:"name,omitempty"` 102 | Policies []PolicyRepresentation `json:"policies,omitempty"` 103 | Resources []ResourceRepresentation `json:"resources,omitempty"` 104 | } 105 | 106 | // ResourceRepresentation represents resources attached to a scope 107 | type ResourceRepresentation struct { 108 | ID string `json:"id,omitempty"` 109 | Attributes AttributeMap `json:"attributes,omitempty"` 110 | DisplayName string `json:"displayName,omitempty"` 111 | IconURI string `json:"iconURI,omitempty"` 112 | Name string `json:"name,omitempty"` 113 | OwnerManagedAccess *bool `json:"ownerManagedAccess,omitempty"` 114 | Scopes []ScopeRepresentation `json:"scopes,omitempty"` 115 | Type string `json:"type,omitempty"` 116 | URI string `json:"uri,omitempty"` 117 | } 118 | 119 | // ProtocolMapperRepresentation represents an individual protocol mapper on a realm client 120 | type ProtocolMapperRepresentation struct { 121 | Config AttributeMap `json:"config,omitempty"` 122 | ID string `json:"id,omitempty"` 123 | Name string `json:"name,omitempty"` 124 | Protocol string `json:"protocol,omitempty"` 125 | ProtocolMapper string `json:"protocolMapper,omitempty"` 126 | } 127 | -------------------------------------------------------------------------------- /keycloak/user_service.go: -------------------------------------------------------------------------------- 1 | package keycloak 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // UserService interacts with all user resources 10 | type UserService service 11 | 12 | // NewUserService returns a new user service for working with user resources 13 | // in a realm. 14 | func NewUserService(c *Client) *UserService { 15 | return &UserService{ 16 | client: c, 17 | } 18 | } 19 | 20 | // Find returns users based on query params 21 | // Params: 22 | // - email 23 | // - first 24 | // - firstName 25 | // - lastName 26 | // - max 27 | // - search 28 | // - userName 29 | func (us *UserService) Find(ctx context.Context, realm string, params map[string]string) ([]UserRepresentation, error) { 30 | 31 | path := "/realms/{realm}/users" 32 | 33 | var user []UserRepresentation 34 | 35 | _, err := us.client.newRequest(ctx). 36 | SetQueryParams(params). 37 | SetPathParams(map[string]string{ 38 | "realm": realm, 39 | }). 40 | SetResult(&user). 41 | Get(path) 42 | 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return user, nil 48 | } 49 | 50 | // Create creates a new user and returns the ID 51 | // Response is a 201 with a location redirect 52 | func (us *UserService) Create(ctx context.Context, realm string, user *UserRepresentation) (string, error) { 53 | path := "/realms/{realm}/users" 54 | 55 | response, err := us.client.newRequest(ctx). 56 | SetPathParams(map[string]string{ 57 | "realm": realm, 58 | }). 59 | SetBody(user). 60 | Post(path) 61 | 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | location, err := url.Parse(response.Header().Get("Location")) 67 | 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | components := strings.Split(location.Path, "/") 73 | 74 | return components[len(components)-1], nil 75 | } 76 | 77 | // Get returns a user in a realm 78 | func (us *UserService) Get(ctx context.Context, realm string, userID string) (*UserRepresentation, error) { 79 | 80 | // nolint: goconst 81 | path := "/realms/{realm}/users/{id}" 82 | 83 | user := &UserRepresentation{} 84 | 85 | _, err := us.client.newRequest(ctx). 86 | SetPathParams(map[string]string{ 87 | "realm": realm, 88 | "id": userID, 89 | }). 90 | SetResult(user). 91 | Get(path) 92 | 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return user, nil 98 | } 99 | 100 | // Update user information 101 | // Response is a 204: No Content 102 | func (us *UserService) Update(ctx context.Context, realm string, user *UserRepresentation) error { 103 | 104 | // nolint: goconst 105 | path := "/realms/{realm}/users/{id}" 106 | 107 | _, err := us.client.newRequest(ctx). 108 | SetPathParams(map[string]string{ 109 | "realm": realm, 110 | "id": user.ID, 111 | }). 112 | SetBody(user). 113 | Put(path) 114 | 115 | return err 116 | 117 | } 118 | 119 | // Delete user information 120 | // Response is a 204: No Content 121 | func (us *UserService) Delete(ctx context.Context, realm string, userID string) error { 122 | 123 | // nolint: goconst 124 | path := "/realms/{realm}/users/{id}" 125 | 126 | _, err := us.client.newRequest(ctx). 127 | SetPathParams(map[string]string{ 128 | "realm": realm, 129 | "id": userID, 130 | }). 131 | Delete(path) 132 | 133 | return err 134 | } 135 | 136 | // Impersonate user 137 | func (us *UserService) Impersonate(ctx context.Context, realm string, userID string) (AttributeMap, error) { 138 | 139 | // nolint: goconst 140 | path := "/realms/{realm}/users/{id}/impersonation" 141 | 142 | a := AttributeMap{} 143 | 144 | _, err := us.client.newRequest(ctx). 145 | SetPathParams(map[string]string{ 146 | "realm": realm, 147 | "id": userID, 148 | }). 149 | SetResult(&a). 150 | Post(path) 151 | 152 | return a, err 153 | } 154 | 155 | // Count gets user count in a realm 156 | func (us *UserService) Count(ctx context.Context, realm string) (uint32, error) { 157 | 158 | // nolint: goconst 159 | path := "/realms/{realm}/users/count" 160 | 161 | var result uint32 162 | 163 | _, err := us.client.newRequest(ctx). 164 | SetPathParams(map[string]string{ 165 | "realm": realm, 166 | }). 167 | SetResult(&result). 168 | Get(path) 169 | 170 | return result, err 171 | } 172 | 173 | // GetGroups gets the groups a realm user belongs to 174 | func (us *UserService) GetGroups(ctx context.Context, realm string, userID string) ([]GroupRepresentation, error) { 175 | 176 | // nolint: goconst 177 | path := "/realms/{realm}/users/{id}/groups" 178 | 179 | var groups []GroupRepresentation 180 | 181 | _, err := us.client.newRequest(ctx). 182 | SetPathParams(map[string]string{ 183 | "realm": realm, 184 | "id": userID, 185 | }). 186 | SetResult(&groups). 187 | Get(path) 188 | 189 | return groups, err 190 | } 191 | 192 | // GetConsents gets consents granted by the user 193 | func (us *UserService) GetConsents(ctx context.Context, realm string, userID string) (AttributeMap, error) { 194 | 195 | // nolint: goconst 196 | path := "/realms/{realm}/users/{id}/consents" 197 | 198 | var consents AttributeMap 199 | 200 | _, err := us.client.newRequest(ctx). 201 | SetPathParams(map[string]string{ 202 | "realm": realm, 203 | "id": userID, 204 | }). 205 | SetResult(&consents). 206 | Get(path) 207 | 208 | return consents, err 209 | } 210 | 211 | // RevokeClientConsents revokes consent and offline tokens for particular client from user 212 | func (us *UserService) RevokeClientConsents(ctx context.Context, realm string, userID string, clientID string) error { 213 | 214 | // nolint: goconst 215 | path := "/realms/{realm}/users/{id}/consents/{client}" 216 | 217 | _, err := us.client.newRequest(ctx). 218 | SetPathParams(map[string]string{ 219 | "realm": realm, 220 | "id": userID, 221 | "client": clientID, 222 | }). 223 | Delete(path) 224 | 225 | return err 226 | } 227 | 228 | // DisableCredentials disables credentials of certain types for a user 229 | func (us *UserService) DisableCredentials(ctx context.Context, realm string, userID string, credentialTypes []string) error { 230 | 231 | // nolint: goconst 232 | path := "/realms/{realm}/users/{id}/disable-credential-types" 233 | 234 | _, err := us.client.newRequest(ctx). 235 | SetPathParams(map[string]string{ 236 | "realm": realm, 237 | "id": userID, 238 | }). 239 | Put(path) 240 | 241 | return err 242 | } 243 | 244 | // AddGroup adds a user to a group 245 | func (us *UserService) AddGroup(ctx context.Context, realm string, userID string, groupID string) error { 246 | 247 | // nolint: goconst 248 | path := "/realms/{realm}/users/{id}/groups/{groupId}" 249 | 250 | _, err := us.client.newRequest(ctx). 251 | SetPathParams(map[string]string{ 252 | "realm": realm, 253 | "id": userID, 254 | "groupId": groupID, 255 | }). 256 | Put(path) 257 | 258 | return err 259 | } 260 | 261 | // RemoveGroup removes a user from a group 262 | func (us *UserService) RemoveGroup(ctx context.Context, realm string, userID string, groupID string) error { 263 | 264 | // nolint: goconst 265 | path := "/realms/{realm}/users/{id}/groups/{groupId}" 266 | 267 | _, err := us.client.newRequest(ctx). 268 | SetPathParams(map[string]string{ 269 | "realm": realm, 270 | "id": userID, 271 | "groupId": groupID, 272 | }). 273 | Delete(path) 274 | 275 | return err 276 | } 277 | 278 | // Logout revokes all user sessions 279 | func (us *UserService) Logout(ctx context.Context, realm string, userID string) error { 280 | 281 | path := "/realms/{realm}/users/{id}/logout" 282 | 283 | _, err := us.client.newRequest(ctx). 284 | SetPathParams(map[string]string{ 285 | "realm": realm, 286 | "id": userID, 287 | }). 288 | Post(path) 289 | 290 | return err 291 | } 292 | 293 | // GetSessions for user 294 | func (us *UserService) GetSessions(ctx context.Context, realm string, userID string) ([]UserSessionRepresentation, error) { 295 | 296 | // nolint: goconst 297 | path := "/realms/{realm}/users/{id}/sessions" 298 | 299 | var sessions []UserSessionRepresentation 300 | 301 | _, err := us.client.newRequest(ctx). 302 | SetPathParams(map[string]string{ 303 | "realm": realm, 304 | "id": userID, 305 | }). 306 | SetResult(&sessions). 307 | Get(path) 308 | 309 | return sessions, err 310 | } 311 | 312 | // GetOfflineSessions for particular client and user 313 | func (us *UserService) GetOfflineSessions(ctx context.Context, realm string, userID string, clientID string) ([]UserSessionRepresentation, error) { 314 | 315 | // nolint: goconst 316 | path := "/realms/{realm}/users/{id}/offline-sessions/{clientId}" 317 | 318 | var sessions []UserSessionRepresentation 319 | 320 | _, err := us.client.newRequest(ctx). 321 | SetPathParams(map[string]string{ 322 | "realm": realm, 323 | "id": userID, 324 | "client": clientID, 325 | }). 326 | SetResult(&sessions). 327 | Get(path) 328 | 329 | return sessions, err 330 | } 331 | 332 | // ResetPassword for user 333 | func (us *UserService) ResetPassword(ctx context.Context, realm string, userID string, tempPassword *CredentialRepresentation) error { 334 | 335 | // nolint: goconst 336 | path := "/realms/{realm}/users/{id}/reset-password" 337 | 338 | _, err := us.client.newRequest(ctx). 339 | SetPathParams(map[string]string{ 340 | "realm": realm, 341 | "id": userID, 342 | }). 343 | SetBody(tempPassword). 344 | Put(path) 345 | 346 | return err 347 | } 348 | -------------------------------------------------------------------------------- /keycloak/realms.go: -------------------------------------------------------------------------------- 1 | //go:generate gomodifytags -file $GOFILE -struct RealmRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 2 | //go:generate gomodifytags -file $GOFILE -struct AuthenticationFlowRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 3 | //go:generate gomodifytags -file $GOFILE -struct AuthenticationExecutionExportRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 4 | //go:generate gomodifytags -file $GOFILE -struct AuthenticatorConfigRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 5 | //go:generate gomodifytags -file $GOFILE -struct ClientScopeRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 6 | //go:generate gomodifytags -file $GOFILE -struct IdentityProviderMapperRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 7 | //go:generate gomodifytags -file $GOFILE -struct IdentityProviderRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 8 | //go:generate gomodifytags -file $GOFILE -struct RequiredActionProviderRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 9 | //go:generate gomodifytags -file $GOFILE -struct RoleRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 10 | //go:generate gomodifytags -file $GOFILE -struct RoleComposites -add-options json=omitempty -add-tags json -w -transform camelcase 11 | //go:generate gomodifytags -file $GOFILE -struct ScopeMappingRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 12 | //go:generate gomodifytags -file $GOFILE -struct UserFederationMapperRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 13 | //go:generate gomodifytags -file $GOFILE -struct UserFederationProviderRepresentation -add-options json=omitempty -add-tags json -w -transform camelcase 14 | 15 | package keycloak 16 | 17 | // RealmRepresentation represents a realm 18 | type RealmRepresentation struct { 19 | AccessCodeLifespan int `json:"accessCodeLifespan,omitempty"` 20 | AccessCodeLifespanLogin int `json:"accessCodeLifespanLogin,omitempty"` 21 | AccessCodeLifespanUserAction int `json:"accessCodeLifespanUserAction,omitempty"` 22 | AccessTokenLifespan int `json:"accessTokenLifespan,omitempty"` 23 | AccessTokenLifespanForImplicitFlow int `json:"accessTokenLifespanForImplicitFlow,omitempty"` 24 | AccountTheme string `json:"accountTheme,omitempty"` 25 | ActionTokenGeneratedByAdminLifespan int `json:"actionTokenGeneratedByAdminLifespan,omitempty"` 26 | ActionTokenGeneratedByUserLifespan int `json:"actionTokenGeneratedByUserLifespan,omitempty"` 27 | AdminEventsDetailsEnabled *bool `json:"adminEventsDetailsEnabled,omitempty"` 28 | AdminEventsEnabled *bool `json:"adminEventsEnabled,omitempty"` 29 | AdminTheme string `json:"adminTheme,omitempty"` 30 | Attributes AttributeMap `json:"attributes,omitempty"` 31 | AuthenticationFlows []AuthenticationFlowRepresentation `json:"authenticationFlows,omitempty"` 32 | AuthenticatorConfig []AuthenticatorConfigRepresentation `json:"authenticatorConfig,omitempty"` 33 | BrowserFlow string `json:"browserFlow,omitempty"` 34 | BrowserSecurityHeaders AttributeMap `json:"browserSecurityHeaders,omitempty"` 35 | BruteForceProtected *bool `json:"bruteForceProtected,omitempty"` 36 | ClientAuthenticationFlow string `json:"clientAuthenticationFlow,omitempty"` 37 | ClientScopeMappings AttributeMap `json:"clientScopeMappings,omitempty"` 38 | ClientScopes []ClientScopeRepresentation `json:"clientScopes,omitempty"` 39 | Clients []ClientRepresentation `json:"clients,omitempty"` 40 | Components MultivaluedHashMap `json:"components,omitempty"` 41 | DefaultDefaultClientScopes []string `json:"defaultDefaultClientScopes,omitempty"` 42 | DefaultGroups []string `json:"defaultGroups,omitempty"` 43 | DefaultLocale string `json:"defaultLocale,omitempty"` 44 | DefaultOptionalClientScopes []string `json:"defaultOptionalClientScopes,omitempty"` 45 | DefaultRoles []string `json:"defaultRoles,omitempty"` 46 | DirectGrantFlow string `json:"directGrantFlow,omitempty"` 47 | DisplayName string `json:"displayName,omitempty"` 48 | DisplayNameHTML string `json:"displayNameHtml,omitempty"` 49 | DockerAuthenticationFlow string `json:"dockerAuthenticationFlow,omitempty"` 50 | DuplicateEmailsAllowed *bool `json:"duplicateEmailsAllowed,omitempty"` 51 | EditUsernameAllowed *bool `json:"editUsernameAllowed,omitempty"` 52 | EmailTheme string `json:"emailTheme,omitempty"` 53 | Enabled *bool `json:"enabled,omitempty"` 54 | EnabledEventTypes []string `json:"enabledEventTypes,omitempty"` 55 | EventsEnabled *bool `json:"eventsEnabled,omitempty"` 56 | EventsExpiration int `json:"eventsExpiration,omitempty"` 57 | EventsListeners []string `json:"eventsListeners,omitempty"` 58 | FailureFactor int `json:"failureFactor,omitempty"` 59 | FederatedUsers []UserRepresentation `json:"federatedUsers,omitempty"` 60 | Groups []GroupRepresentation `json:"groups,omitempty"` 61 | ID string `json:"id,omitempty"` 62 | IdentityProviderMappers []IdentityProviderMapperRepresentation `json:"identityProviderMappers,omitempty"` 63 | IdentityProviders []IdentityProviderRepresentation `json:"identityProviders,omitempty"` 64 | InternationalizationEnabled *bool `json:"internationalizationEnabled,omitempty"` 65 | KeycloakVersion string `json:"keycloakVersion,omitempty"` 66 | LoginTheme string `json:"loginTheme,omitempty"` 67 | LoginWithEmailAllowed *bool `json:"loginWithEmailAllowed,omitempty"` 68 | MaxDeltaTimeSeconds int `json:"maxDeltaTimeSeconds,omitempty"` 69 | MaxFailureWaitSeconds int `json:"maxFailureWaitSeconds,omitempty"` 70 | MinimumQuickLoginWaitSeconds int `json:"minimumQuickLoginWaitSeconds,omitempty"` 71 | NotBefore int `json:"notBefore,omitempty"` 72 | OfflineSessionIdleTimeout int `json:"offlineSessionIdleTimeout,omitempty"` 73 | OtpPolicyAlgorithm string `json:"otpPolicyAlgorithm,omitempty"` 74 | OtpPolicyDigits int `json:"otpPolicyDigits,omitempty"` 75 | OtpPolicyLookAheadWindow int `json:"otpPolicyLookAheadWindow,omitempty"` 76 | OtpPolicyPeriod int `json:"otpPolicyPeriod,omitempty"` 77 | OtpPolicyType string `json:"otpPolicyType,omitempty"` 78 | OtpSupportedApplications []string `json:"otpSupportedApplications,omitempty"` 79 | PasswordPolicy string `json:"passwordPolicy,omitempty"` 80 | PermanentLockout *bool `json:"permanentLockout,omitempty"` 81 | ProtocolMappers []ProtocolMapperRepresentation `json:"protocolMappers,omitempty"` 82 | QuickLoginCheckMilliSeconds int `json:"quickLoginCheckMilliSeconds,omitempty"` 83 | Realm string `json:"realm,omitempty"` 84 | RefreshTokenMaxReuse int `json:"refreshTokenMaxReuse,omitempty"` 85 | RegistrationAllowed *bool `json:"registrationAllowed,omitempty"` 86 | RegistrationEmailAsUsername *bool `json:"registrationEmailAsUsername,omitempty"` 87 | RegistrationFlow string `json:"registrationFlow,omitempty"` 88 | RememberMe *bool `json:"rememberMe,omitempty"` 89 | RequiredActions []RequiredActionProviderRepresentation `json:"requiredActions,omitempty"` 90 | ResetCredentialsFlow string `json:"resetCredentialsFlow,omitempty"` 91 | ResetPasswordAllowed *bool `json:"resetPasswordAllowed,omitempty"` 92 | RevokeRefreshToken *bool `json:"revokeRefreshToken,omitempty"` 93 | Roles RolesRepresentation `json:"roles,omitempty"` 94 | ScopeMappings []ScopeMappingRepresentation `json:"scopeMappings,omitempty"` 95 | SMTPServer AttributeMap `json:"smtpServer,omitempty"` 96 | SslRequired string `json:"sslRequired,omitempty"` 97 | SsoSessionIdleTimeout int `json:"ssoSessionIdleTimeout,omitempty"` 98 | SsoSessionMaxLifespan int `json:"ssoSessionMaxLifespan,omitempty"` 99 | SupportedLocales []string `json:"supportedLocales,omitempty"` 100 | UserFederationMappers []UserFederationMapperRepresentation `json:"userFederationMappers,omitempty"` 101 | UserFederationProviders []UserFederationProviderRepresentation `json:"userFederationProviders,omitempty"` 102 | UserManagedAccessAllowed *bool `json:"userManagedAccessAllowed,omitempty"` 103 | Users []UserRepresentation `json:"users,omitempty"` 104 | VerifyEmail *bool `json:"verifyEmail,omitempty"` 105 | WaitIncrementSeconds int `json:"waitIncrementSeconds,omitempty"` 106 | } 107 | 108 | // AuthenticationFlowRepresentation for representing Flows 109 | type AuthenticationFlowRepresentation struct { 110 | Alias string `json:"alias,omitempty"` 111 | AuthenticationExecutions []AuthenticationExecutionExportRepresentation `json:"authenticationExecutions,omitempty"` 112 | BuiltIn *bool `json:"builtIn,omitempty"` 113 | Description string `json:"description,omitempty"` 114 | ID string `json:"id,omitempty"` 115 | ProviderID string `json:"providerID,omitempty"` 116 | TopLevel *bool `json:"topLevel,omitempty"` 117 | } 118 | 119 | // AuthenticationExecutionExportRepresentation for Authenticator Execution 120 | type AuthenticationExecutionExportRepresentation struct { 121 | Authenticator string `json:"authenticator,omitempty"` 122 | AuthenticatorConfig string `json:"authenticatorConfig,omitempty"` 123 | AuthenticatorFlow *bool `json:"authenticatorFlow,omitempty"` 124 | AutheticatorFlow *bool `json:"autheticatorFlow,omitempty"` 125 | FlowAlias string `json:"flowAlias,omitempty"` 126 | Priority int `json:"priority,omitempty"` 127 | Requirement string `json:"requirement,omitempty"` 128 | UserSetupAllowed *bool `json:"userSetupAllowed,omitempty"` 129 | } 130 | 131 | // AuthenticatorConfigRepresentation Authenticator Config 132 | type AuthenticatorConfigRepresentation struct { 133 | Alias string `json:"alias,omitempty"` 134 | Config AttributeMap `json:"config,omitempty"` 135 | ID string `json:"id,omitempty"` 136 | } 137 | 138 | // ClientScopeRepresentation Client Scope 139 | type ClientScopeRepresentation struct { 140 | Attributes AttributeMap `json:"attributes,omitempty"` 141 | Description string `json:"description,omitempty"` 142 | ID string `json:"id,omitempty"` 143 | Name string `json:"name,omitempty"` 144 | Protocol string `json:"protocol,omitempty"` 145 | ProtocolMappers []ProtocolMapperRepresentation `json:"protocolMappers,omitempty"` 146 | } 147 | 148 | // IdentityProviderMapperRepresentation Identity Provider Mapper 149 | type IdentityProviderMapperRepresentation struct { 150 | Config AttributeMap `json:"config,omitempty"` 151 | ID string `json:"id,omitempty"` 152 | IdentityProviderAlias string `json:"identityProviderAlias,omitempty"` 153 | IdentityProviderMapper string `json:"identityProviderMapper,omitempty"` 154 | Name string `json:"name,omitempty"` 155 | } 156 | 157 | // IdentityProviderRepresentation Identity Provider 158 | type IdentityProviderRepresentation struct { 159 | AddReadTokenRoleOnCreate *bool `json:"addReadTokenRoleOnCreate,omitempty"` 160 | Alias string `json:"alias,omitempty"` 161 | Config AttributeMap `json:"config,omitempty"` 162 | DisplayName string `json:"displayName,omitempty"` 163 | Enabled *bool `json:"enabled,omitempty"` 164 | FirstBrokerLoginFlowAlias string `json:"firstBrokerLoginFlowAlias,omitempty"` 165 | InternalID string `json:"internalID,omitempty"` 166 | LinkOnly *bool `json:"linkOnly,omitempty"` 167 | PostBrokerLoginFlowAlias string `json:"postBrokerLoginFlowAlias,omitempty"` 168 | ProviderID string `json:"providerID,omitempty"` 169 | StoreToken *bool `json:"storeToken,omitempty"` 170 | TrustEmail *bool `json:"trustEmail,omitempty"` 171 | } 172 | 173 | // RequiredActionProviderRepresentation Required Action Provider 174 | type RequiredActionProviderRepresentation struct { 175 | Alias string `json:"alias,omitempty"` 176 | Config AttributeMap `json:"config,omitempty"` 177 | DefaultAction *bool `json:"defaultAction,omitempty"` 178 | Enabled *bool `json:"enabled,omitempty"` 179 | Name string `json:"name,omitempty"` 180 | ProviderID string `json:"providerID,omitempty"` 181 | } 182 | 183 | // RolesRepresentation Roles Representation 184 | type RolesRepresentation struct { 185 | Client AttributeMap `json:"client,omitempty"` 186 | Realm []RoleRepresentation `json:"realm,omitempty"` 187 | } 188 | 189 | // RoleRepresentation Role 190 | type RoleRepresentation struct { 191 | ClientRole *bool `json:"clientRole,omitempty"` 192 | Composite *bool `json:"composite,omitempty"` 193 | Composites RoleComposites `json:"composites,omitempty"` 194 | ContainerID string `json:"containerID,omitempty"` 195 | Description string `json:"description,omitempty"` 196 | ID string `json:"id,omitempty"` 197 | Name string `json:"name,omitempty"` 198 | } 199 | 200 | // RoleComposites known in keycloak as a "RoleRepresentations-Composites" in 201 | // in the source it is just an inner-class. 202 | type RoleComposites struct { 203 | Client AttributeMap `json:"client,omitempty"` 204 | Realm []string `json:"realm,omitempty"` 205 | } 206 | 207 | // ScopeMappingRepresentation Scope Mapping 208 | type ScopeMappingRepresentation struct { 209 | Client string `json:"client,omitempty"` 210 | ClientScope string `json:"clientScope,omitempty"` 211 | Roles []string `json:"roles,omitempty"` 212 | Self string `json:"self,omitempty"` 213 | } 214 | 215 | // UserFederationMapperRepresentation User Federation 216 | type UserFederationMapperRepresentation struct { 217 | Config AttributeMap `json:"config,omitempty"` 218 | FederationMapperType string `json:"federationMapperType,omitempty"` 219 | FederationProviderDisplayName string `json:"federationProviderDisplayName,omitempty"` 220 | ID string `json:"id,omitempty"` 221 | Name string `json:"name,omitempty"` 222 | } 223 | 224 | // UserFederationProviderRepresentation User federation provider 225 | type UserFederationProviderRepresentation struct { 226 | ChangedSyncPeriod int32 `json:"changedSyncPeriod,omitempty"` 227 | Config AttributeMap `json:"config,omitempty"` 228 | DisplayName string `json:"displayName,omitempty"` 229 | FullSyncPeriod int32 `json:"fullSyncPeriod,omitempty"` 230 | ID string `json:"id,omitempty"` 231 | LastSync int `json:"lastSync,omitempty"` 232 | Priority int32 `json:"priority,omitempty"` 233 | ProviderName string `json:"providerName,omitempty"` 234 | } 235 | --------------------------------------------------------------------------------